Browse Source

chore: initial commit

e22m4u 2 years ago
commit
dbff18d833
100 changed files with 25143 additions and 0 deletions
  1. 9 0
      .c8rc
  2. 5 0
      .commitlintrc
  3. 13 0
      .editorconfig
  4. 20 0
      .eslintrc.cjs
  5. 17 0
      .gitignore
  6. 4 0
      .husky/commit-msg
  7. 9 0
      .husky/pre-commit
  8. 7 0
      .mocharc.cjs
  9. 7 0
      .prettierrc
  10. 21 0
      LICENSE
  11. 536 0
      README.md
  12. 10 0
      mocha.setup.js
  13. 51 0
      package.json
  14. 63 0
      src/adapter/adapter-loader.js
  15. 32 0
      src/adapter/adapter-loader.spec.js
  16. 32 0
      src/adapter/adapter-registry.js
  17. 36 0
      src/adapter/adapter-registry.spec.js
  18. 178 0
      src/adapter/adapter.js
  19. 143 0
      src/adapter/adapter.spec.js
  20. 334 0
      src/adapter/builtin/memory-adapter.js
  21. 2925 0
      src/adapter/builtin/memory-adapter.spec.js
  22. 44 0
      src/adapter/decorator/data-sanitizing-decorator.js
  23. 59 0
      src/adapter/decorator/data-sanitizing-decorator.spec.js
  24. 41 0
      src/adapter/decorator/data-validation-decorator.js
  25. 59 0
      src/adapter/decorator/data-validation-decorator.spec.js
  26. 57 0
      src/adapter/decorator/default-values-decorator.js
  27. 141 0
      src/adapter/decorator/default-values-decorator.spec.js
  28. 72 0
      src/adapter/decorator/fields-filtering-decorator.js
  29. 119 0
      src/adapter/decorator/fields-filtering-decorator.spec.js
  30. 78 0
      src/adapter/decorator/inclusion-decorator.js
  31. 117 0
      src/adapter/decorator/inclusion-decorator.spec.js
  32. 5 0
      src/adapter/decorator/index.js
  33. 3 0
      src/adapter/index.js
  34. 33 0
      src/definition/datasource/datasource-definition-validator.js
  35. 64 0
      src/definition/datasource/datasource-definition-validator.spec.js
  36. 1 0
      src/definition/datasource/index.js
  37. 98 0
      src/definition/definition-registry.js
  38. 78 0
      src/definition/definition-registry.spec.js
  39. 3 0
      src/definition/index.js
  40. 6 0
      src/definition/model/index.js
  41. 34 0
      src/definition/model/model-data-sanitizer.js
  42. 144 0
      src/definition/model/model-data-validator.js
  43. 1885 0
      src/definition/model/model-data-validator.spec.js
  44. 360 0
      src/definition/model/model-definition-utils.js
  45. 1474 0
      src/definition/model/model-definition-utils.spec.js
  46. 83 0
      src/definition/model/model-definition-validator.js
  47. 150 0
      src/definition/model/model-definition-validator.spec.js
  48. 20 0
      src/definition/model/properties/data-type.js
  49. 53 0
      src/definition/model/properties/default-values-definition-validator.js
  50. 136 0
      src/definition/model/properties/default-values-definition-validator.spec.js
  51. 4 0
      src/definition/model/properties/index.js
  52. 55 0
      src/definition/model/properties/primary-keys-definition-validator.js
  53. 145 0
      src/definition/model/properties/primary-keys-definition-validator.spec.js
  54. 189 0
      src/definition/model/properties/properties-definition-validator.js
  55. 374 0
      src/definition/model/properties/properties-definition-validator.spec.js
  56. 2 0
      src/definition/model/relations/index.js
  57. 16 0
      src/definition/model/relations/relation-type.js
  58. 450 0
      src/definition/model/relations/relations-definition-validator.js
  59. 773 0
      src/definition/model/relations/relations-definition-validator.spec.js
  60. 3 0
      src/errors/index.js
  61. 20 0
      src/errors/invalid-argument-error.js
  62. 33 0
      src/errors/invalid-argument-error.spec.js
  63. 25 0
      src/errors/invalid-operator-value-error.js
  64. 11 0
      src/errors/invalid-operator-value-error.spec.js
  65. 20 0
      src/errors/not-implemented-error.js
  66. 33 0
      src/errors/not-implemented-error.spec.js
  67. 84 0
      src/filter/fields-clause-tool.js
  68. 134 0
      src/filter/fields-clause-tool.spec.js
  69. 362 0
      src/filter/include-clause-tool.js
  70. 653 0
      src/filter/include-clause-tool.spec.js
  71. 6 0
      src/filter/index.js
  72. 503 0
      src/filter/operator-clause-tool.js
  73. 1064 0
      src/filter/operator-clause-tool.spec.js
  74. 94 0
      src/filter/order-clause-tool.js
  75. 439 0
      src/filter/order-clause-tool.spec.js
  76. 64 0
      src/filter/slice-clause-tool.js
  77. 119 0
      src/filter/slice-clause-tool.spec.js
  78. 159 0
      src/filter/where-clause-tool.js
  79. 281 0
      src/filter/where-clause-tool.spec.js
  80. 9 0
      src/index.js
  81. 236 0
      src/relations/belongs-to-resolver.js
  82. 1047 0
      src/relations/belongs-to-resolver.spec.js
  83. 313 0
      src/relations/has-many-resolver.js
  84. 2911 0
      src/relations/has-many-resolver.spec.js
  85. 307 0
      src/relations/has-one-resolver.js
  86. 2274 0
      src/relations/has-one-resolver.spec.js
  87. 4 0
      src/relations/index.js
  88. 111 0
      src/relations/references-many-resolver.js
  89. 631 0
      src/relations/references-many-resolver.spec.js
  90. 3 0
      src/repository/index.js
  91. 165 0
      src/repository/repository-observer.js
  92. 455 0
      src/repository/repository-observer.spec.js
  93. 54 0
      src/repository/repository-registry.js
  94. 38 0
      src/repository/repository-registry.spec.js
  95. 373 0
      src/repository/repository.js
  96. 125 0
      src/repository/repository.spec.js
  97. 37 0
      src/schema.js
  98. 1 0
      src/service/index.js
  99. 28 0
      src/service/service.js
  100. 37 0
      src/service/service.spec.js

+ 9 - 0
.c8rc

@@ -0,0 +1,9 @@
+{
+  "all": true,
+  "include": [
+    "src/**/*.js"
+  ],
+  "exclude": [
+    "src/**/*.spec.js"
+  ]
+}

+ 5 - 0
.commitlintrc

@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "@commitlint/config-conventional"
+  ]
+}

+ 13 - 0
.editorconfig

@@ -0,0 +1,13 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
+max_line_length = 80

+ 20 - 0
.eslintrc.cjs

@@ -0,0 +1,20 @@
+module.exports = {
+  env: {
+    es2021: true,
+    node: true
+  },
+  parserOptions: {
+    sourceType: 'module',
+    ecmaVersion: 13,
+  },
+  plugins: [
+    'mocha',
+    'chai-expect',
+  ],
+  extends: [
+    'eslint:recommended',
+    'prettier',
+    'plugin:mocha/recommended',
+    'plugin:chai-expect/recommended',
+  ],
+}

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+# OS
+.DS_Store
+
+# IDE
+.idea
+
+# Npm
+node_modules
+npm-debug.log
+package-lock.json
+
+# Yarn
+.yarn/
+.yarn*
+
+# c8
+coverage

+ 4 - 0
.husky/commit-msg

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no -- commitlint --edit "${1}"

+ 9 - 0
.husky/pre-commit

@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npm run lint:fix
+npm run format
+
+npm run test
+
+git add -A

+ 7 - 0
.mocharc.cjs

@@ -0,0 +1,7 @@
+const path = require('path');
+
+module.exports = {
+  extension: ['js'],
+  spec: 'src/**/*.spec.js',
+  require: [path.join(__dirname, 'mocha.setup.js')],
+}

+ 7 - 0
.prettierrc

@@ -0,0 +1,7 @@
+{
+  "bracketSpacing": false,
+  "singleQuote": true,
+  "printWidth": 80,
+  "trailingComma": "all",
+  "arrowParens": "avoid"
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 e22m4u@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 536 - 0
README.md

@@ -0,0 +1,536 @@
+## @e22m4u/repository
+
+Абстракция для работы с базами данных для Node.js  
+  
+  
+| адаптер | описание                                                                                                                                     |
+|---------|----------------------------------------------------------------------------------------------------------------------------------------------|
+| memory  | хранение данных в памяти процесса Node.js для разработки и тестирования                                                                      |
+| mongodb | MongoDB - система управления NoSQL базами данных (*[требует установки](https://www.npmjs.com/package/@e22m4u/repository-mongodb-adapter))* |
+
+## Установка
+
+```bash
+npm install @e22m4u/repository
+```
+
+Опционально устанавливаем адаптер, например [mongodb](https://www.npmjs.com/package/@e22m4u/repository-mongodb-adapter)
+
+```bash
+npm install @e22m4u/repository-mongodb-adapter
+```
+
+## Пример
+
+Создаем экземпляр класса `Schema`
+
+```js
+import {Schema} from '@e22m4u/repository';
+
+const schema = new Schema();
+```
+
+Объявляем источник данных `myDatasource`
+
+```js
+schema.defineDatasource({
+  name: 'myDatasource', // название источника
+  adapter: 'memory', // выбранный адаптер
+});
+```
+
+Объявляем модель заказа `order`
+
+```js
+schema.defineModel({
+  name: 'order', // название модели
+  datasource: 'myDatasource', // используемый источник
+  properties: { // поля модели
+    cost: 'number', // поле типа "number"
+  },
+  relations: { // связи с другими моделями
+    customer: { // название связи
+      type: 'belongsTo', // хранить идентификатор цели у себя
+      model: 'customer', // название целевой модели
+      foreignKey: 'customerId', // в каком поле хранить идентификатор цели
+    },
+  },
+});
+```
+
+Объявляем модель покупателя `customer`
+
+```js
+schema.defineModel({
+  name: 'customer', // название модели
+  datasource: 'myDatasource', // используемый источник
+  properties: { // поля модели
+    name: { // название поля
+      type: 'string', // тип поля
+      required: true, // сделать поле обязательным
+    },
+  },
+  relations: { // связи с другими моделями
+    orders: { // название связи
+      type: 'hasMany', // искать свой идентификатор в целевой модели
+      model: 'order', // целевая модель
+      foreignKey: 'customerId', // в каком поле искать идентификатор
+    },
+  },
+});
+```
+
+Получаем репозитории для моделей `customer` и `order`
+
+```js
+const customerRep = schema.getRepository('customer');
+const orderRep = schema.getRepository('order')
+```
+
+Создаем покупателя `fedor` и его заказы
+
+```js
+// создаем покупателя
+const fedor = await customerRep.create({name: 'Fedor'});
+
+// и заказы
+await orderRep.create({cost: 150, customerId: fedor.id});
+await orderRep.create({cost: 250, customerId: fedor.id});
+```
+
+Получаем покупателя методом `findById`, включая связь с заказами
+
+```js
+const fedorWithOrders = await customerRep.findById(
+  fedor.id, // искомый идентификатор
+  {include: ['orders']}, // имена включаемых связей
+);
+// {
+//   id: 1,
+//   name: 'Fedor',
+//   orders: [
+//     {id: 1: cost: 150, customerId: 1},
+//     {id: 2: cost: 250, customerId: 1},
+//   ],
+// }
+```
+
+Получаем заказы и их покупателей методом `find`
+
+```js
+const ordersWithCustomers = await orderRep.find({
+  // опциональные параметры
+  where: {customerId: fedor.id}, // фильтрация
+  order: ['id DESC'], // сортировка результата
+  limit: 10, // ограничение количества
+  skip: 0, // пропуск записей
+  fields: ['cost', 'customerId'], // запрашиваемые поля
+  include: ['customer'], // включаемые связи
+});
+console.log(ordersWithCustomers);
+// [
+//   {
+//     id: 1,
+//     cost: 150,
+//     customerId: 1,
+//     customer: {
+//       id: 1,
+//       name: 'Fedor',
+//     },
+//   },
+//   {
+//     id: 2,
+//     cost: 250,
+//     customerId: 1,
+//     customer: {
+//       id: 1,
+//       name: 'Fedor',
+//     },
+//   },
+// ]
+```
+
+Удаляем данные методами `deleteById` и `delete`
+
+```js
+// удаляем покупателя по идентификатору
+await customerRep.deleteById(fedor.id); // true
+
+// удаляем заказы по условию
+await orderRep.delete({customerId: fedor.id}); // 2
+```
+
+## Datasource
+
+Определяет настройки и способ подключения к базе.
+
+Параметры:
+
+- `name: string` название нового источника
+- `adapter: string` выбранный адаптер базы данных
+
+Пример:
+
+```js
+schema.defineDatasource({
+  name: 'myDatasource',
+  adapter: 'memory',
+});
+```
+
+Адаптер может иметь параметры, которые передаются
+при определении источника.
+
+Пример:
+
+```js
+schema.defineDatasource({
+  name: 'myDatasource',
+  adapter: 'mongodb',
+  // параметры адаптера:
+  host: '127.0.0.1',
+  port: 27017,
+});
+```
+
+## Model
+
+Описывает набор полей и связей к другим моделям.
+
+Параметры:
+
+- `name: string` название новой модели
+- `datasource: string` выбранный источник данных
+- `properties: object` определения полей модели
+- `relations: object` определения связей модели
+
+Пример:
+
+```js
+schema.defineModel({
+  name: 'myModel',
+  datasource: 'myDatasource',
+  properties: {...}, // см. ниже
+  relations: {...}, // см. ниже
+});
+```
+
+## Properties
+
+Описывает набор полей и их настройки.
+
+Типы:
+
+- `string`  
+- `number`  
+- `boolean`  
+- `array`  
+- `object`  
+- `any`  
+
+Пример:
+
+```js
+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`
+
+Пример:
+
+```js
+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`
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    customer: {
+      type: 'belongsTo',
+      model: 'customer',
+      foreignKey: 'customerId', // опционально
+    },
+  },
+});
+```
+
+Полиморфная версия
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    customer: {
+      type: 'belongsTo',
+      polymorphic: true,
+      foreignKey: 'customerId', // опционально
+      discriminator: 'customerType', // опционально
+    },
+  },
+});
+```
+
+#### HasOne (one-to-one)
+
+Связь покупателя к заказу, как обратная сторона `belongsTo`
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    order: {
+      type: 'hasOne',
+      model: 'order',
+      foreignKey: 'customerId', // опционально
+    },
+  },
+});
+```
+
+Обратная сторона полиморфной версии `belongsTo`
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    order: {
+      type: 'hasOne',
+      model: 'order',
+      polymorphic: 'customer', // имя связи целевой модели
+    },
+  },
+});
+```
+
+Явное указание `foreignKey` и `discriminator`
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    order: {
+      type: 'hasOne',
+      model: 'order',
+      polymorphic: true, // true вместо имени модели
+      foreignKey: 'customerId', // поле целевой модели 
+      discriminator: 'customerType', // поле целевой модели
+    },
+  },
+});
+```
+
+#### HasMany (one-to-many)
+
+Связь покупателя к заказам, как обратная сторона `belongsTo`
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    orders: {
+      type: 'hasMany',
+      model: 'order',
+      foreignKey: 'customerId', // опционально
+    },
+  },
+});
+```
+
+Обратная сторона полиморфной версии `belongsTo`
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    orders: {
+      type: 'hasMany',
+      model: 'order',
+      polymorphic: 'customer', // имя связи целевой модели
+    },
+  },
+});
+```
+
+Явное указание `foreignKey` и `discriminator`
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    orders: {
+      type: 'hasMany',
+      model: 'order',
+      polymorphic: true, // true вместо имени модели
+      foreignKey: 'customerId', // поле целевой модели 
+      discriminator: 'customerType', // поле целевой модели
+    },
+  },
+});
+```
+
+#### ReferencesMany
+
+Связь покупателя к заказам через поле `orderIds`
+
+```js
+schema.defineModel({
+  // ...
+  relations: {
+    // ...
+    orders: {
+      type: 'referencesMany',
+      model: 'order',
+      foreignKey: 'orderIds', // опционально
+    },
+  },
+});
+```
+
+## Repository
+
+Выполняет операции чтения и записи определенной модели.
+
+Методы:
+
+- `create(data, filter = undefined)`
+- `replaceById(id, data, filter = undefined)`
+- `replaceOrCreate(data, filter = 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)`
+
+Получение репозитория модели:
+
+```js
+import {Schema} from '@e22m4u/repository';
+
+const schema = new Schema();
+// создаем источник
+schema.defineDatasource({name: 'myDatasource', adapter: 'memory'});
+// создаем модель
+schema.defineModel({name: 'myModel', datasource: 'myDatasource'});
+// получаем репозиторий по названию модели
+const repositorty = schema.getRepository('myModel');
+```
+
+Переопределение конструктора:
+
+```js
+import {Schema} from '@e22m4u/repository';
+import {Repository} from '@e22m4u/repository';
+import {RepositoryRegistry} from '@e22m4u/repository';
+
+class MyRepository extends Repository {
+  /*...*/
+}
+
+const schema = new Schema();
+schema.get(RepositoryRegistry).setRepositoryCtor(MyRepository);
+// теперь schema.getRepository(modelName) будет возвращать
+// экземпляр класса MyRepository
+```
+
+## Тесты
+
+```bash
+npm run test
+```
+
+## Лицензия
+
+MIT

+ 10 - 0
mocha.setup.js

@@ -0,0 +1,10 @@
+import chai from 'chai';
+import chaiSpies from 'chai-spies';
+import chaiSubset from 'chai-subset';
+import chaiAsPromised from 'chai-as-promised';
+
+process.env['NODE_ENV'] = 'test';
+
+chai.use(chaiSpies);
+chai.use(chaiSubset);
+chai.use(chaiAsPromised);

+ 51 - 0
package.json

@@ -0,0 +1,51 @@
+{
+  "name": "@e22m4u/repository",
+  "version": "0.0.18",
+  "description": "Абстракция для работы с базами данных для Node.js",
+  "type": "module",
+  "main": "src/index.js",
+  "engines": {
+    "node": ">=14"
+  },
+  "scripts": {
+    "lint": "eslint .",
+    "lint:fix": "eslint . --fix",
+    "format": "prettier --write \"./src/**/*.js\"",
+    "test": "eslint . && c8 --reporter=text-summary mocha",
+    "test:coverage": "eslint . && c8 --reporter=text mocha",
+    "prepare": "npx husky install"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/e22m4u/repository"
+  },
+  "keywords": [
+    "Repository",
+    "ORM",
+    "ODM",
+    "Database",
+    "Datasource",
+    "Relation",
+    "Inclusion",
+    "Memory"
+  ],
+  "author": "e22m4u <e22m4u@gmail.com>",
+  "license": "MIT",
+  "homepage": "https://github.com/e22m4u/repository",
+  "devDependencies": {
+    "@commitlint/cli": "^17.7.1",
+    "@commitlint/config-conventional": "^17.7.0",
+    "c8": "^8.0.1",
+    "chai": "^4.3.7",
+    "chai-as-promised": "^7.1.1",
+    "chai-spies": "^1.0.0",
+    "chai-subset": "^1.6.0",
+    "eslint": "^8.47.0",
+    "eslint-config-prettier": "^9.0.0",
+    "eslint-plugin-chai-expect": "^3.0.0",
+    "eslint-plugin-mocha": "^10.1.0",
+    "husky": "^8.0.3",
+    "mocha": "^10.2.0",
+    "prettier": "^3.0.1"
+  }
+}

+ 63 - 0
src/adapter/adapter-loader.js

@@ -0,0 +1,63 @@
+import {Adapter} from './adapter.js';
+import {Service} from '../service/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+
+/**
+ * Adapter loader.
+ */
+export class AdapterLoader extends Service {
+  /**
+   * Load by name.
+   *
+   * @param {string} adapterName
+   * @param {object} settings
+   * @return {Promise<any>}
+   */
+  async loadByName(adapterName, settings = undefined) {
+    if (!adapterName || typeof adapterName !== 'string')
+      throw new InvalidArgumentError(
+        'The adapter name must be a non-empty String, but %s given.',
+        adapterName,
+      );
+    let adapterCtor;
+    try {
+      const module = await import(`./builtin/${adapterName}-adapter.js`);
+      adapterCtor = findAdapterCtorInModule(module);
+    } catch (e) {
+      /**/
+    }
+    if (!adapterCtor)
+      try {
+        const module = await import(
+          `@e22m4u/repository-${adapterName}-adapter`
+        );
+        adapterCtor = findAdapterCtorInModule(module);
+      } catch (e) {
+        /**/
+      }
+    if (!adapterCtor)
+      throw new InvalidArgumentError(
+        'The adapter %s is not found.',
+        adapterName,
+      );
+    return new adapterCtor(this._services, settings);
+  }
+}
+
+/**
+ * Find adapter ctor in module.
+ *
+ * @param module
+ * @return {*}
+ */
+function findAdapterCtorInModule(module) {
+  let adapterCtor;
+  if (!module || typeof module !== 'object' || Array.isArray(module)) return;
+  for (const ctor of Object.values(module)) {
+    if (typeof ctor === 'function' && ctor.prototype instanceof Adapter) {
+      adapterCtor = ctor;
+      break;
+    }
+  }
+  return adapterCtor;
+}

+ 32 - 0
src/adapter/adapter-loader.spec.js

@@ -0,0 +1,32 @@
+import {expect} from 'chai';
+import {Adapter} from './adapter.js';
+import {AdapterLoader} from './adapter-loader.js';
+
+const S = new AdapterLoader();
+
+describe('AdapterLoader', function () {
+  describe('loadByName', function () {
+    it('requires an adapter name as a non-empty string', async function () {
+      const promise = S.loadByName('');
+      await expect(promise).to.be.rejectedWith(
+        'The adapter name must be a non-empty String, but "" given.',
+      );
+    });
+
+    it('throws an error if a given adapter name is not found', async function () {
+      const promise = S.loadByName('unknown');
+      await expect(promise).to.be.rejectedWith(
+        'The adapter "unknown" is not found.',
+      );
+    });
+
+    it('returns an adapter instance that is loaded from "builtin" folder', async function () {
+      const settings = {};
+      const adapter = await S.loadByName('memory', settings);
+      const services = S._services;
+      expect(adapter).to.be.instanceof(Adapter);
+      expect(adapter._services).to.be.eq(services);
+      expect(adapter.settings).to.be.eq(settings);
+    });
+  });
+});

+ 32 - 0
src/adapter/adapter-registry.js

@@ -0,0 +1,32 @@
+import {Service} from '../service/index.js';
+import {AdapterLoader} from './adapter-loader.js';
+import {DefinitionRegistry} from '../definition/index.js';
+
+/**
+ * Adapter registry.
+ */
+export class AdapterRegistry extends Service {
+  /**
+   * Adapters.
+   *
+   * @type {{[name: string]: object}}
+   */
+  _adapters = {};
+
+  /**
+   * Get adapter.
+   *
+   * @param datasourceName
+   * @return {Promise<object>}
+   */
+  async getAdapter(datasourceName) {
+    let adapter = this._adapters[datasourceName];
+    if (adapter) return adapter;
+    const datasource =
+      this.get(DefinitionRegistry).getDatasource(datasourceName);
+    const adapterName = datasource.adapter;
+    adapter = await this.get(AdapterLoader).loadByName(adapterName, datasource);
+    this._adapters[datasourceName] = adapter;
+    return adapter;
+  }
+}

+ 36 - 0
src/adapter/adapter-registry.spec.js

@@ -0,0 +1,36 @@
+import {expect} from 'chai';
+import {AdapterRegistry} from './adapter-registry.js';
+import {MemoryAdapter} from './builtin/memory-adapter.js';
+import {Schema} from '../schema.js';
+
+describe('AdapterRegistry', function () {
+  describe('getAdapter', function () {
+    it('instantiates a new or returns an existing adapter by a given datasource name', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      const R = S.get(AdapterRegistry);
+      const adapter = await R.getAdapter('datasource');
+      expect(adapter).to.be.instanceof(MemoryAdapter);
+      const sameAdapter = await R.getAdapter('datasource');
+      expect(sameAdapter).to.be.eq(adapter);
+    });
+
+    it('throws an error if a datasource is not exists', async function () {
+      const R = new AdapterRegistry();
+      const promise = R.getAdapter('unknown');
+      await expect(promise).to.be.rejectedWith(
+        'The datasource "unknown" is not defined.',
+      );
+    });
+
+    it('throws an error if an adapter is not exists', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'unknown'});
+      const R = S.get(AdapterRegistry);
+      const promise = R.getAdapter('datasource');
+      await expect(promise).to.be.rejectedWith(
+        'The adapter "unknown" is not found.',
+      );
+    });
+  });
+});

+ 178 - 0
src/adapter/adapter.js

@@ -0,0 +1,178 @@
+/* eslint no-unused-vars: 0 */
+import {Service} from '../service/index.js';
+import {NotImplementedError} from '../errors/index.js';
+import {InclusionDecorator} from './decorator/index.js';
+import {DefaultValuesDecorator} from './decorator/index.js';
+import {DataValidationDecorator} from './decorator/index.js';
+import {DataSanitizingDecorator} from './decorator/index.js';
+import {FieldsFilteringDecorator} from './decorator/index.js';
+
+/**
+ * Adapter.
+ */
+export class Adapter extends Service {
+  /**
+   * Settings.
+   */
+  _settings;
+
+  /**
+   * Settings.
+   *
+   * @return {*}
+   */
+  get settings() {
+    return this._settings;
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param services
+   * @param settings
+   */
+  constructor(services = undefined, settings = undefined) {
+    super(services);
+    this._settings = settings;
+    // decorate only extended classes
+    if (this.constructor !== Adapter) {
+      this.get(DataValidationDecorator).decorate(this);
+      this.get(DataSanitizingDecorator).decorate(this);
+      this.get(DefaultValuesDecorator).decorate(this);
+      this.get(FieldsFilteringDecorator).decorate(this);
+      this.get(InclusionDecorator).decorate(this);
+    }
+  }
+
+  /**
+   * Create.
+   *
+   * @param {string} modelName
+   * @param {Record<string, unknown>} modelData
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object>}
+   */
+  create(modelName, modelData, filter = undefined) {
+    throw new NotImplementedError(
+      '%s.create is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+
+  /**
+   * Replace by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {Record<string, unknown>} modelData
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object>}
+   */
+  replaceById(modelName, id, modelData, filter = undefined) {
+    throw new NotImplementedError(
+      '%s.replaceById is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+
+  /**
+   * Patch by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {Record<string, unknown>} modelData
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object>}
+   */
+  patchById(modelName, id, modelData, filter = undefined) {
+    throw new NotImplementedError(
+      '%s.patchById is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+
+  /**
+   * Find.
+   *
+   * @param {string} modelName
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object[]>}
+   */
+  find(modelName, filter = undefined) {
+    throw new NotImplementedError(
+      '%s.find is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+
+  /**
+   * Find by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object>}
+   */
+  findById(modelName, id, filter = undefined) {
+    throw new NotImplementedError(
+      '%s.findById is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+
+  /**
+   * Delete.
+   *
+   * @param {string} modelName
+   * @param {Record<string, unknown>|undefined} where
+   * @return {Promise<number>}
+   */
+  delete(modelName, where = undefined) {
+    throw new NotImplementedError(
+      '%s.delete is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+
+  /**
+   * Delete by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @return {Promise<boolean>}
+   */
+  deleteById(modelName, id) {
+    throw new NotImplementedError(
+      '%s.deleteById is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+
+  /**
+   * Exists.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @return {Promise<boolean>}
+   */
+  exists(modelName, id) {
+    throw new NotImplementedError(
+      '%s.exists is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+
+  /**
+   * Count.
+   *
+   * @param {string} modelName
+   * @param {Record<string, unknown>|undefined} where
+   * @return {Promise<number>}
+   */
+  count(modelName, where = undefined) {
+    throw new NotImplementedError(
+      '%s.count is not implemented.',
+      new String(this.constructor.name),
+    );
+  }
+}

+ 143 - 0
src/adapter/adapter.spec.js

@@ -0,0 +1,143 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {Schema} from '../schema.js';
+import {Adapter} from './adapter.js';
+import {Service} from '../service/index.js';
+import {InclusionDecorator} from './decorator/index.js';
+import {DefaultValuesDecorator} from './decorator/index.js';
+import {DataValidationDecorator} from './decorator/index.js';
+import {DataSanitizingDecorator} from './decorator/index.js';
+import {FieldsFilteringDecorator} from './decorator/index.js';
+
+const sandbox = chai.spy.sandbox();
+
+describe('Adapter', function () {
+  describe('constructor', function () {
+    afterEach(function () {
+      sandbox.restore();
+    });
+
+    it('inherits from the Service class', function () {
+      const adapter = new Adapter();
+      expect(adapter).to.be.instanceof(Service);
+    });
+
+    it('sets given services an settings', function () {
+      const services = new Map();
+      const settings = {};
+      const adapter = new Adapter(services, settings);
+      expect(adapter._services).to.be.eq(services);
+      expect(adapter._settings).to.be.eq(settings);
+    });
+
+    it('decorates only extended adapter', function () {
+      const schema = new Schema();
+      const dec1 = schema.get(DataValidationDecorator);
+      const dec2 = schema.get(DataSanitizingDecorator);
+      const dec3 = schema.get(DefaultValuesDecorator);
+      const dec4 = schema.get(FieldsFilteringDecorator);
+      const dec5 = schema.get(InclusionDecorator);
+      const order = [];
+      const decorate = function (ctx) {
+        expect(ctx).to.be.instanceof(Adapter);
+        order.push(this);
+      };
+      sandbox.on(dec1, 'decorate', decorate);
+      sandbox.on(dec2, 'decorate', decorate);
+      sandbox.on(dec3, 'decorate', decorate);
+      sandbox.on(dec4, 'decorate', decorate);
+      sandbox.on(dec5, 'decorate', decorate);
+      new Adapter(schema._services);
+      expect(order).to.be.empty;
+      expect(dec1.decorate).to.be.not.called;
+      expect(dec2.decorate).to.be.not.called;
+      expect(dec3.decorate).to.be.not.called;
+      expect(dec4.decorate).to.be.not.called;
+      expect(dec5.decorate).to.be.not.called;
+      class ExtendedAdapter extends Adapter {}
+      new ExtendedAdapter(schema._services);
+      expect(order[0]).to.be.eql(dec1);
+      expect(order[1]).to.be.eql(dec2);
+      expect(order[2]).to.be.eql(dec3);
+      expect(order[3]).to.be.eql(dec4);
+      expect(order[4]).to.be.eql(dec5);
+      expect(dec1.decorate).to.be.called.once;
+      expect(dec2.decorate).to.be.called.once;
+      expect(dec3.decorate).to.be.called.once;
+      expect(dec4.decorate).to.be.called.once;
+      expect(dec5.decorate).to.be.called.once;
+    });
+  });
+
+  describe('create', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.create();
+      expect(throwable).to.throw('Adapter.create is not implemented.');
+    });
+  });
+
+  describe('replaceById', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.replaceById();
+      expect(throwable).to.throw('Adapter.replaceById is not implemented.');
+    });
+  });
+
+  describe('patchById', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.patchById();
+      expect(throwable).to.throw('Adapter.patchById is not implemented.');
+    });
+  });
+
+  describe('find', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.find();
+      expect(throwable).to.throw('Adapter.find is not implemented.');
+    });
+  });
+
+  describe('findById', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.findById();
+      expect(throwable).to.throw('Adapter.findById is not implemented.');
+    });
+  });
+
+  describe('delete', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.delete();
+      expect(throwable).to.throw('Adapter.delete is not implemented.');
+    });
+  });
+
+  describe('deleteById', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.deleteById();
+      expect(throwable).to.throw('Adapter.deleteById is not implemented.');
+    });
+  });
+
+  describe('exists', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.exists();
+      expect(throwable).to.throw('Adapter.exists is not implemented.');
+    });
+  });
+
+  describe('count', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.count();
+      expect(throwable).to.throw('Adapter.count is not implemented.');
+    });
+  });
+});

+ 334 - 0
src/adapter/builtin/memory-adapter.js

@@ -0,0 +1,334 @@
+import {Adapter} from '../adapter.js';
+import {cloneDeep} from '../../utils/index.js';
+import {capitalize} from '../../utils/index.js';
+import {DataType} from '../../definition/index.js';
+import {SliceClauseTool} from '../../filter/index.js';
+import {WhereClauseTool} from '../../filter/index.js';
+import {OrderClauseTool} from '../../filter/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {ModelDefinitionUtils} from '../../definition/index.js';
+
+/**
+ * Memory adapter.
+ */
+export class MemoryAdapter extends Adapter {
+  /**
+   * Tables.
+   *
+   * @type {Map<string, Map<number, Record<string, any>>>}
+   */
+  _tables = new Map();
+
+  /**
+   * Last ids.
+   *
+   * @type {Map<string, number>}
+   */
+  _lastIds = new Map();
+
+  /**
+   * Get table or create.
+   *
+   * @param {string} modelName
+   * @return {Map<number, Record<string, any>>}
+   */
+  _getTableOrCreate(modelName) {
+    const tableName =
+      this.get(ModelDefinitionUtils).getTableNameByModelName(modelName);
+    let table = this._tables.get(tableName);
+    if (table) return table;
+    table = new Map();
+    this._tables.set(tableName, table);
+    return table;
+  }
+
+  /**
+   * Gen next id value.
+   *
+   * @param {string} modelName
+   * @param {string} propName
+   * @return {number}
+   */
+  _genNextIdValue(modelName, propName) {
+    const propType = this.get(ModelDefinitionUtils).getDataTypeByPropertyName(
+      modelName,
+      propName,
+    );
+    if (propType !== DataType.ANY && propType !== DataType.NUMBER)
+      throw new InvalidArgumentError(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key %s of the model %s is defined as %s. ' +
+          'Do provide your own value for the %s property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+        propName,
+        modelName,
+        new String(capitalize(propType)),
+        propName,
+      );
+    const tableName =
+      this.get(ModelDefinitionUtils).getTableNameByModelName(modelName);
+    const lastId = this._lastIds.get(tableName) ?? 0;
+    const nextId = lastId + 1;
+    this._lastIds.set(tableName, nextId);
+    const table = this._getTableOrCreate(modelName);
+    const existedIds = Array.from(table.keys());
+    if (existedIds.includes(nextId))
+      return this._genNextIdValue(modelName, propName);
+    return nextId;
+  }
+
+  /**
+   * Create
+   *
+   * @param {string} modelName
+   * @param {Record<string, unknown>} modelData
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object>}
+   */
+  // eslint-disable-next-line no-unused-vars
+  async create(modelName, modelData, filter = undefined) {
+    const pkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(modelName);
+    let idValue = modelData[pkPropName];
+    if (idValue == null) idValue = this._genNextIdValue(modelName, pkPropName);
+
+    const table = this._getTableOrCreate(modelName);
+    if (table.has(idValue))
+      throw new InvalidArgumentError(
+        'The value %s of the primary key %s already exists in the model %s.',
+        idValue,
+        pkPropName,
+        modelName,
+      );
+
+    modelData = cloneDeep(modelData);
+    modelData[pkPropName] = idValue;
+
+    const tableData = this.get(
+      ModelDefinitionUtils,
+    ).convertPropertyNamesToColumnNames(modelName, modelData);
+    table.set(idValue, tableData);
+
+    return this.get(ModelDefinitionUtils).convertColumnNamesToPropertyNames(
+      modelName,
+      tableData,
+    );
+  }
+
+  /**
+   * Replace by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {Record<string, unknown>} modelData
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object>}
+   */
+  // eslint-disable-next-line no-unused-vars
+  async replaceById(modelName, id, modelData, filter = undefined) {
+    const table = this._getTableOrCreate(modelName);
+    const isExists = table.has(id);
+    const pkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(modelName);
+    if (!isExists)
+      throw new InvalidArgumentError(
+        'The value %s of the primary key %s does not exist in the model %s.',
+        id,
+        pkPropName,
+        modelName,
+      );
+
+    modelData = cloneDeep(modelData);
+    modelData[pkPropName] = id;
+
+    const tableData = this.get(
+      ModelDefinitionUtils,
+    ).convertPropertyNamesToColumnNames(modelName, modelData);
+    table.set(id, tableData);
+
+    return this.get(ModelDefinitionUtils).convertColumnNamesToPropertyNames(
+      modelName,
+      tableData,
+    );
+  }
+
+  /**
+   * Patch by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {Record<string, unknown>} modelData
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object>}
+   */
+  // eslint-disable-next-line no-unused-vars
+  async patchById(modelName, id, modelData, filter = undefined) {
+    const table = this._getTableOrCreate(modelName);
+    const existingTableData = table.get(id);
+    const pkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(modelName);
+    if (existingTableData == null)
+      throw new InvalidArgumentError(
+        'The value %s of the primary key %s does not exist in the model %s.',
+        id,
+        pkPropName,
+        modelName,
+      );
+
+    modelData = cloneDeep(modelData);
+    delete modelData[pkPropName];
+
+    const existingModelData = this.get(
+      ModelDefinitionUtils,
+    ).convertColumnNamesToPropertyNames(modelName, existingTableData);
+    const mergedModelData = Object.assign({}, existingModelData, modelData);
+    const mergedTableData = this.get(
+      ModelDefinitionUtils,
+    ).convertPropertyNamesToColumnNames(modelName, mergedModelData);
+    table.set(id, mergedTableData);
+
+    return this.get(ModelDefinitionUtils).convertColumnNamesToPropertyNames(
+      modelName,
+      mergedTableData,
+    );
+  }
+
+  /**
+   * Find.
+   *
+   * @param {string} modelName
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object[]>}
+   */
+  async find(modelName, filter = undefined) {
+    const table = this._getTableOrCreate(modelName);
+    const tableItems = Array.from(table.values());
+    let modelItems = tableItems.map(tableItem =>
+      this.get(ModelDefinitionUtils).convertColumnNamesToPropertyNames(
+        modelName,
+        tableItem,
+      ),
+    );
+
+    if (filter && typeof filter === 'object') {
+      if (filter.where)
+        modelItems = this.get(WhereClauseTool).filter(modelItems, filter.where);
+      if (filter.skip || filter.limit)
+        modelItems = this.get(SliceClauseTool).slice(
+          modelItems,
+          filter.skip,
+          filter.limit,
+        );
+      if (filter.order)
+        this.get(OrderClauseTool).sort(modelItems, filter.order);
+    }
+    return modelItems;
+  }
+
+  /**
+   * Find by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @param {Record<string, unknown>|undefined} filter
+   * @return {Promise<object>}
+   */
+  // eslint-disable-next-line no-unused-vars
+  async findById(modelName, id, filter = undefined) {
+    const table = this._getTableOrCreate(modelName);
+    const tableData = table.get(id);
+    const pkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(modelName);
+    if (!tableData)
+      throw new InvalidArgumentError(
+        'The value %s of the primary key %s does not exist in the model %s.',
+        id,
+        pkPropName,
+        modelName,
+      );
+    return this.get(ModelDefinitionUtils).convertColumnNamesToPropertyNames(
+      modelName,
+      tableData,
+    );
+  }
+
+  /**
+   * Delete.
+   *
+   * @param {string} modelName
+   * @param {Record<string, unknown>|undefined} where
+   * @return {Promise<number>}
+   */
+  async delete(modelName, where = undefined) {
+    const table = this._getTableOrCreate(modelName);
+    const tableItems = Array.from(table.values());
+    if (!tableItems.length) return 0;
+    let modelItems = tableItems.map(tableItem =>
+      this.get(ModelDefinitionUtils).convertColumnNamesToPropertyNames(
+        modelName,
+        tableItem,
+      ),
+    );
+
+    if (where && typeof where === 'object')
+      modelItems = this.get(WhereClauseTool).filter(modelItems, where);
+    const size = modelItems.length;
+
+    const idPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(modelName);
+    modelItems.forEach(modelData => {
+      const idValue = modelData[idPropName];
+      table.delete(idValue);
+    });
+    return size;
+  }
+
+  /**
+   * Delete by id.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @return {Promise<boolean>}
+   */
+  async deleteById(modelName, id) {
+    const table = this._getTableOrCreate(modelName);
+    const isExists = table.has(id);
+    table.delete(id);
+    return isExists;
+  }
+
+  /**
+   * Exists.
+   *
+   * @param {string} modelName
+   * @param {string|number} id
+   * @return {Promise<boolean>}
+   */
+  async exists(modelName, id) {
+    const table = this._getTableOrCreate(modelName);
+    return table.has(id);
+  }
+
+  /**
+   * Count.
+   *
+   * @param {string} modelName
+   * @param {Record<string, unknown>|undefined} where
+   * @return {Promise<number>}
+   */
+  async count(modelName, where = undefined) {
+    const table = this._getTableOrCreate(modelName);
+    const tableItems = Array.from(table.values());
+    let modelItems = tableItems.map(tableItem =>
+      this.get(ModelDefinitionUtils).convertColumnNamesToPropertyNames(
+        modelName,
+        tableItem,
+      ),
+    );
+
+    if (where && typeof where === 'object')
+      modelItems = this.get(WhereClauseTool).filter(modelItems, where);
+    return modelItems.length;
+  }
+}

+ 2925 - 0
src/adapter/builtin/memory-adapter.spec.js

@@ -0,0 +1,2925 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {Schema} from '../../schema.js';
+import {MemoryAdapter} from './memory-adapter.js';
+import {DataType} from '../../definition/index.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../../definition/index.js';
+
+describe('MemoryAdapter', function () {
+  describe('_getTableOrCreate', function () {
+    it('returns an existing table or creates a new', function () {
+      const S = new Schema();
+      S.defineModel({name: 'model'});
+      const A = S.get(MemoryAdapter);
+      const table = A._getTableOrCreate('model');
+      expect(table).to.be.instanceof(Map);
+      const sameTable = A._getTableOrCreate('model');
+      expect(table).to.be.eq(sameTable);
+    });
+
+    it('uses a model name to find a table, even a table name is specified', function () {
+      const S = new Schema();
+      S.defineModel({
+        name: 'myModel',
+        tableName: 'myTable',
+      });
+      const A = S.get(MemoryAdapter);
+      const table = A._getTableOrCreate('myModel');
+      expect(table).to.be.instanceof(Map);
+      const sameTable = A._getTableOrCreate('myModel');
+      expect(table).to.be.eq(sameTable);
+    });
+
+    it('stores a table by specified table name', function () {
+      const S = new Schema();
+      S.defineModel({
+        name: 'myModel',
+        tableName: 'myTable',
+      });
+      const A = S.get(MemoryAdapter);
+      const table = A._getTableOrCreate('myModel');
+      expect(table).to.be.instanceof(Map);
+      const sameTable = A._tables.get('myTable');
+      expect(table).to.be.eq(sameTable);
+    });
+  });
+
+  describe('_genNextIdValue', function () {
+    it('returns an unique number identifier', function () {
+      const S = new Schema();
+      S.defineModel({name: 'model'});
+      const A = S.get(MemoryAdapter);
+      const id1 = A._genNextIdValue('model', DEF_PK);
+      const id2 = A._genNextIdValue('model', DEF_PK);
+      const id3 = A._genNextIdValue('model', DEF_PK);
+      expect(id1).to.be.eq(1);
+      expect(id2).to.be.eq(2);
+      expect(id3).to.be.eq(3);
+    });
+  });
+
+  describe('create', function () {
+    it('skips existing values when generating a new identifier for a default primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result1 = await adapter.create('model', {});
+      const result2 = await adapter.create('model', {});
+      const result3 = await adapter.create('model', {[DEF_PK]: 3});
+      const result4 = await adapter.create('model', {});
+      expect(result1).to.be.eql({[DEF_PK]: 1});
+      expect(result2).to.be.eql({[DEF_PK]: 2});
+      expect(result3).to.be.eql({[DEF_PK]: 3});
+      expect(result4).to.be.eql({[DEF_PK]: 4});
+    });
+
+    it('skips existing values when generating a new identifier for a specified primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result1 = await adapter.create('model', {});
+      const result2 = await adapter.create('model', {});
+      const result3 = await adapter.create('model', {myId: 3});
+      const result4 = await adapter.create('model', {});
+      expect(result1).to.be.eql({myId: 1});
+      expect(result2).to.be.eql({myId: 2});
+      expect(result3).to.be.eql({myId: 3});
+      expect(result4).to.be.eql({myId: 4});
+    });
+
+    it('generates a new identifier when a value of a primary key has not provided', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10};
+      const created = await adapter.create('model', input);
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({...input, [DEF_PK]: idValue});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({...input, [DEF_PK]: idValue});
+    });
+
+    it('generates a new identifier when a value of a primary key is undefined', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {
+        [DEF_PK]: undefined,
+        foo: 'string',
+        bar: 10,
+      };
+      const created = await adapter.create('model', input);
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({...input, [DEF_PK]: idValue});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({...input, [DEF_PK]: idValue});
+    });
+
+    it('generates a new identifier when a value of a primary key is null', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {
+        [DEF_PK]: null,
+        foo: 'string',
+        bar: 10,
+      };
+      const created = await adapter.create('model', input);
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({...input, [DEF_PK]: idValue});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({...input, [DEF_PK]: idValue});
+    });
+
+    it('generates a new identifier for a primary key of a "number" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result1 = await adapter.create('model', {});
+      const result2 = await adapter.create('model', {});
+      const result3 = await adapter.create('model', {});
+      expect(result1).to.be.eql({myId: 1});
+      expect(result2).to.be.eql({myId: 2});
+      expect(result3).to.be.eql({myId: 3});
+    });
+
+    it('generates a new identifier for a primary key of an "any" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.ANY,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result1 = await adapter.create('model', {});
+      const result2 = await adapter.create('model', {});
+      const result3 = await adapter.create('model', {});
+      expect(result1).to.be.eql({myId: 1});
+      expect(result2).to.be.eql({myId: 2});
+      expect(result3).to.be.eql({myId: 3});
+    });
+
+    it('throws an error when generating a new value for a primary key of a "string" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const promise = adapter.create('model', {foo: 'string', bar: 10});
+      await expect(promise).to.be.rejectedWith(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key "myId" of the model "model" is defined as String. ' +
+          'Do provide your own value for the "myId" property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of a "boolean" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.BOOLEAN,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const promise = adapter.create('model', {foo: 'string', bar: 10});
+      await expect(promise).to.be.rejectedWith(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key "myId" of the model "model" is defined as Boolean. ' +
+          'Do provide your own value for the "myId" property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of an "array" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.ARRAY,
+            itemType: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const promise = adapter.create('model', {});
+      await expect(promise).to.be.rejectedWith(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key "myId" of the model "model" is defined as Array. ' +
+          'Do provide your own value for the "myId" property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of an "object" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.OBJECT,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const promise = adapter.create('model', {});
+      await expect(promise).to.be.rejectedWith(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key "myId" of the model "model" is defined as Object. ' +
+          'Do provide your own value for the "myId" property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+      );
+    });
+
+    it('allows to specify an identifier value for a new item', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const idValue = 5;
+      const input = {foo: 'string', bar: 10};
+      const created = await adapter.create('model', {
+        [DEF_PK]: idValue,
+        ...input,
+      });
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('throws an error if a given identifier value already exists', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {foo: 'string'});
+      const promise = adapter.create('model', created);
+      await expect(promise).to.be.rejectedWith(
+        format(
+          'The value 1 of the primary key "%s" already exists in the model "model".',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('sets default values if they are not provided for a new item', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 10,
+          },
+          bar: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 10, bar: 'string'};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('sets default values for properties provided with an undefined value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {foo: undefined});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('sets default values for properties provided with a null value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {foo: null});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('uses a specified column name for a primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            columnName: 'bar',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {});
+      expect(created).to.be.eql({foo: created.foo});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(created.foo);
+      expect(tableData).to.be.eql({bar: created.foo});
+    });
+
+    it('uses a specified column name for a regular property', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'bar',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {foo: 10});
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({[DEF_PK]: idValue, foo: 10});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, bar: 10});
+    });
+
+    it('uses a specified column name for a regular property with a default value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'bar',
+            default: 10,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({[DEF_PK]: idValue, foo: 10});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, bar: 10});
+    });
+
+    it('uses a short form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10};
+      const filter = {fields: 'foo'};
+      const result = await adapter.create('model', input, filter);
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: input.foo});
+    });
+
+    it('uses a full form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+          baz: DataType.BOOLEAN,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      const filter = {fields: ['foo', 'bar']};
+      const result = await adapter.create('model', input, filter);
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: input.foo,
+        bar: input.bar,
+      });
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.BOOLEAN,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      const filter = {fields: ['foo', 'bar']};
+      const result = await adapter.create('model', input, filter);
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: input.foo,
+        bar: input.bar,
+      });
+    });
+  });
+
+  describe('replaceById', function () {
+    it('removes properties when replacing an item by a given identifier', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 1, bar: 2};
+      const created = await adapter.create('model', input);
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...input});
+      const replaced = await adapter.replaceById('model', idValue, {foo: 2});
+      expect(replaced).to.be.eql({[DEF_PK]: idValue, foo: 2});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, foo: 2});
+    });
+
+    it('ignores identifier value in a given data in case of a default primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const createdModelData = await adapter.create('model', {[DEF_PK]: 10});
+      expect(createdModelData).to.be.eql({[DEF_PK]: 10});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(10);
+      expect(createdTableData).to.be.eql({[DEF_PK]: 10});
+      const replacedModelData = await adapter.replaceById('model', 10, {
+        [DEF_PK]: 20,
+      });
+      expect(replacedModelData).to.be.eql({[DEF_PK]: 10});
+      const replacedTableData = table.get(10);
+      expect(replacedTableData).to.be.eql({[DEF_PK]: 10});
+    });
+
+    it('ignores identifier value in a given data in case of a specified primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const createdModelData = await adapter.create('model', {myId: 10});
+      expect(createdModelData).to.be.eql({myId: 10});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(10);
+      expect(createdTableData).to.be.eql({myId: 10});
+      const replacedModelData = await adapter.replaceById('model', 10, {
+        myId: 20,
+      });
+      expect(replacedModelData).to.be.eql({myId: 10});
+      const replacedTableData = table.get(10);
+      expect(replacedTableData).to.be.eql({myId: 10});
+    });
+
+    it('sets a default values for removed properties when replacing an item', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const replacing = {foo: 2};
+      const replaced = await adapter.replaceById('model', idValue, replacing);
+      expect(replaced).to.be.eql({
+        [DEF_PK]: idValue,
+        ...defaults,
+        ...replacing,
+      });
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({
+        [DEF_PK]: idValue,
+        ...defaults,
+        ...replacing,
+      });
+    });
+
+    it('sets a default values for replaced properties with an undefined value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const replaced = await adapter.replaceById('model', idValue, {
+        foo: undefined,
+      });
+      expect(replaced).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('sets a default values for replaced properties with a null value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const replaced = await adapter.replaceById('model', idValue, {
+        foo: null,
+      });
+      expect(replaced).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('throws an error if a given identifier does not exist', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const promise = adapter.replaceById('model', 1, {foo: 2});
+      await expect(promise).to.be.rejectedWith(
+        format(
+          'The value 1 of the primary key "%s" does not exist in the model "model".',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('uses a specified column name for a primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            columnName: 'qux',
+          },
+          bar: DataType.NUMBER,
+          baz: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {bar: 1, baz: 2};
+      const createdModelData = await adapter.create('model', input);
+      expect(createdModelData).to.be.eql({
+        foo: createdModelData.foo,
+        ...input,
+      });
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(createdModelData.foo);
+      expect(createdTableData).to.be.eql({
+        qux: createdModelData.foo,
+        ...input,
+      });
+      const replacing = {bar: 2};
+      const replacedModelData = await adapter.replaceById(
+        'model',
+        createdModelData.foo,
+        replacing,
+      );
+      expect(replacedModelData).to.be.eql({
+        foo: createdModelData.foo,
+        ...replacing,
+      });
+      const replacedTableData = table.get(createdModelData.foo);
+      expect(replacedTableData).to.be.eql({
+        qux: createdModelData.foo,
+        ...replacing,
+      });
+    });
+
+    it('uses a specified column name for a regular property', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'baz',
+          },
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 1, bar: 2};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        baz: input.foo,
+        bar: input.bar,
+      });
+      const replacing = {foo: 2};
+      const replacedModelData = await adapter.replaceById(
+        'model',
+        idValue,
+        replacing,
+      );
+      expect(replacedModelData).to.be.eql({[DEF_PK]: idValue, ...replacing});
+      const replacedTableData = table.get(idValue);
+      expect(replacedTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        baz: replacing.foo,
+      });
+    });
+
+    it('uses a specified column name for a regular property with a default value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'baz',
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const createdModelData = await adapter.create('model', {});
+      const idValue = createdModelData[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        baz: defaults.foo,
+        bar: defaults.bar,
+      });
+      const replacing = {foo: 2};
+      const replacedModelData = await adapter.replaceById(
+        'model',
+        idValue,
+        replacing,
+      );
+      expect(replacedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: replacing.foo,
+        bar: defaults.bar,
+      });
+      const replacedTableData = table.get(idValue);
+      expect(replacedTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        baz: replacing.foo,
+        bar: defaults.bar,
+      });
+    });
+
+    it('allows to specify a short form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const replacedModelData = await adapter.replaceById(
+        'model',
+        idValue,
+        input,
+        {fields: 'foo'},
+      );
+      expect(replacedModelData).to.be.eql({[DEF_PK]: idValue, foo: input.foo});
+      const replacedTableData = table.get(idValue);
+      expect(replacedTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('allows to specify a full form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+          baz: DataType.BOOLEAN,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const replacedModelData = await adapter.replaceById(
+        'model',
+        idValue,
+        input,
+        {fields: ['foo', 'bar']},
+      );
+      expect(replacedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      const replacedTableData = table.get(idValue);
+      expect(replacedTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.BOOLEAN,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        fooCol: input.foo,
+        barCol: input.bar,
+        bazCol: input.baz,
+      });
+      const replacedModelData = await adapter.replaceById(
+        'model',
+        idValue,
+        input,
+        {fields: ['foo', 'bar']},
+      );
+      expect(replacedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      const replacedTableData = table.get(idValue);
+      expect(replacedTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        fooCol: input.foo,
+        barCol: input.bar,
+        bazCol: input.baz,
+      });
+    });
+  });
+
+  describe('patchById', function () {
+    it('updates only provided properties by a given identifier', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 1, bar: 2};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const patch = {foo: 20};
+      const patchedModelData = await adapter.patchById('model', idValue, patch);
+      expect(patchedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: patch.foo,
+        bar: input.bar,
+      });
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: patch.foo,
+        bar: input.bar,
+      });
+    });
+
+    it('does not throw an error if a partial data does not have required property', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            required: true,
+          },
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 1, bar: 2};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const patch = {bar: 20};
+      const patchedModelData = await adapter.patchById('model', idValue, patch);
+      expect(patchedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: input.foo,
+        bar: patch.bar,
+      });
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: input.foo,
+        bar: patch.bar,
+      });
+    });
+
+    it('ignores identifier value in a given data in case of a default primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const idValue = 10;
+      const createdModelData = await adapter.create('model', {
+        [DEF_PK]: idValue,
+      });
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue});
+      const patchedModelData = await adapter.patchById('model', idValue, {
+        [DEF_PK]: 20,
+      });
+      expect(patchedModelData).to.be.eql({[DEF_PK]: idValue});
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({[DEF_PK]: idValue});
+    });
+
+    it('ignores identifier value in a given data in case of a specified primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const idValue = 10;
+      const createdModelData = await adapter.create('model', {myId: idValue});
+      expect(createdModelData).to.be.eql({myId: idValue});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({myId: idValue});
+      const patchedModelData = await adapter.patchById('model', idValue, {
+        myId: 20,
+      });
+      expect(patchedModelData).to.be.eql({myId: idValue});
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({myId: idValue});
+    });
+
+    it('sets a default values for patched properties with an undefined value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const createdModelData = await adapter.create('model', {});
+      const idValue = createdModelData[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const patchedModelData = await adapter.patchById('model', idValue, {
+        foo: undefined,
+      });
+      expect(patchedModelData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('sets a default values for patched properties with a null value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const createdModelData = await adapter.create('model', {});
+      const idValue = createdModelData[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const patchedModelData = await adapter.patchById('model', idValue, {
+        foo: null,
+      });
+      expect(patchedModelData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('throws an error if a given identifier does not exist', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const promise = adapter.patchById('model', 1, {foo: 2});
+      await expect(promise).to.be.rejectedWith(
+        format(
+          'The value 1 of the primary key "%s" does not exist in the model "model".',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('uses a specified column name for a primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            columnName: 'qux',
+          },
+          bar: DataType.NUMBER,
+          baz: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {bar: 1, baz: 2};
+      const createdModelData = await adapter.create('model', input);
+      expect(createdModelData).to.be.eql({foo: createdModelData.foo, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(createdModelData.foo);
+      expect(createdTableData).to.be.eql({qux: createdModelData.foo, ...input});
+      const patching = {bar: 2};
+      const patchedModelData = await adapter.patchById(
+        'model',
+        createdModelData.foo,
+        patching,
+      );
+      expect(patchedModelData).to.be.eql({
+        foo: createdModelData.foo,
+        bar: patching.bar,
+        baz: input.baz,
+      });
+      const patchedTableData = table.get(createdModelData.foo);
+      expect(patchedTableData).to.be.eql({
+        qux: createdModelData.foo,
+        bar: patching.bar,
+        baz: input.baz,
+      });
+    });
+
+    it('uses a specified column name for a regular property', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'baz',
+          },
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 1, bar: 2};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        baz: input.foo,
+        bar: input.bar,
+      });
+      const patching = {foo: 2};
+      const patchedModelData = await adapter.patchById(
+        'model',
+        idValue,
+        patching,
+      );
+      expect(patchedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: patching.foo,
+        bar: input.bar,
+      });
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        baz: patching.foo,
+        bar: input.bar,
+      });
+    });
+
+    it('uses a specified column name for a regular property with a default value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'baz',
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const createdModelData = await adapter.create('model', {});
+      const idValue = createdModelData[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        baz: defaults.foo,
+        bar: defaults.bar,
+      });
+      const patching = {foo: 2};
+      const patchedModelData = await adapter.patchById(
+        'model',
+        idValue,
+        patching,
+      );
+      expect(patchedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: patching.foo,
+        bar: defaults.bar,
+      });
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        baz: patching.foo,
+        bar: defaults.bar,
+      });
+    });
+
+    it('allows to specify a short form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const patchedModelData = await adapter.patchById(
+        'model',
+        idValue,
+        input,
+        {fields: 'foo'},
+      );
+      expect(patchedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: input.foo,
+      });
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('allows to specify a full form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+          baz: DataType.BOOLEAN,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const patchedModelData = await adapter.patchById(
+        'model',
+        idValue,
+        input,
+        {fields: ['foo', 'bar']},
+      );
+      expect(patchedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.BOOLEAN,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      const createdModelData = await adapter.create('model', input);
+      const idValue = createdModelData[DEF_PK];
+      expect(createdModelData).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const createdTableData = table.get(idValue);
+      expect(createdTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        fooCol: input.foo,
+        barCol: input.bar,
+        bazCol: input.baz,
+      });
+      const patchedModelData = await adapter.patchById(
+        'model',
+        idValue,
+        input,
+        {fields: ['foo', 'bar']},
+      );
+      expect(patchedModelData).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      const patchedTableData = table.get(idValue);
+      expect(patchedTableData).to.be.eql({
+        [DEF_PK]: idValue,
+        fooCol: input.foo,
+        barCol: input.bar,
+        bazCol: input.baz,
+      });
+    });
+  });
+
+  describe('find', function () {
+    it('returns an empty array if a table does not have an items', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result = await adapter.find('model');
+      expect(result).to.be.instanceof(Array);
+      expect(result).to.be.empty;
+    });
+
+    it('returns an array of table items', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {});
+      await adapter.create('model', {});
+      await adapter.create('model', {});
+      const result = await adapter.find('model');
+      expect(result).to.be.instanceof(Array);
+      expect(result).to.have.lengthOf(3);
+      expect(result[0]).to.be.eql({[DEF_PK]: 1});
+      expect(result[1]).to.be.eql({[DEF_PK]: 2});
+      expect(result[2]).to.be.eql({[DEF_PK]: 3});
+    });
+
+    it('uses default values for non-existent properties', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      table.set(1, {[DEF_PK]: 1});
+      table.set(2, {[DEF_PK]: 2});
+      table.set(3, {[DEF_PK]: 3});
+      const result = await adapter.find('model');
+      expect(result).to.be.instanceof(Array);
+      expect(result).to.have.lengthOf(3);
+      expect(result[0]).to.be.eql({[DEF_PK]: 1, foo: 'string'});
+      expect(result[1]).to.be.eql({[DEF_PK]: 2, foo: 'string'});
+      expect(result[2]).to.be.eql({[DEF_PK]: 3, foo: 'string'});
+      const tableItems = Array.from(table.values());
+      expect(tableItems).to.have.lengthOf(3);
+      expect(tableItems[0]).to.be.eql({[DEF_PK]: 1});
+      expect(tableItems[1]).to.be.eql({[DEF_PK]: 2});
+      expect(tableItems[2]).to.be.eql({[DEF_PK]: 3});
+    });
+
+    it('uses default values for properties of an undefined', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      table.set(1, {[DEF_PK]: 1, foo: undefined});
+      table.set(2, {[DEF_PK]: 2, foo: undefined});
+      table.set(3, {[DEF_PK]: 3, foo: undefined});
+      const result = await adapter.find('model');
+      expect(result).to.be.instanceof(Array);
+      expect(result).to.have.lengthOf(3);
+      expect(result[0]).to.be.eql({[DEF_PK]: 1, foo: 'string'});
+      expect(result[1]).to.be.eql({[DEF_PK]: 2, foo: 'string'});
+      expect(result[2]).to.be.eql({[DEF_PK]: 3, foo: 'string'});
+      const tableItems = Array.from(table.values());
+      expect(tableItems).to.have.lengthOf(3);
+      expect(tableItems[0]).to.be.eql({[DEF_PK]: 1, foo: undefined});
+      expect(tableItems[1]).to.be.eql({[DEF_PK]: 2, foo: undefined});
+      expect(tableItems[2]).to.be.eql({[DEF_PK]: 3, foo: undefined});
+    });
+
+    it('uses default values for properties of a null', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      table.set(1, {[DEF_PK]: 1, foo: null});
+      table.set(2, {[DEF_PK]: 2, foo: null});
+      table.set(3, {[DEF_PK]: 3, foo: null});
+      const result = await adapter.find('model');
+      expect(result).to.be.instanceof(Array);
+      expect(result).to.have.lengthOf(3);
+      expect(result[0]).to.be.eql({[DEF_PK]: 1, foo: 'string'});
+      expect(result[1]).to.be.eql({[DEF_PK]: 2, foo: 'string'});
+      expect(result[2]).to.be.eql({[DEF_PK]: 3, foo: 'string'});
+      const tableItems = Array.from(table.values());
+      expect(tableItems).to.have.lengthOf(3);
+      expect(tableItems[0]).to.be.eql({[DEF_PK]: 1, foo: null});
+      expect(tableItems[1]).to.be.eql({[DEF_PK]: 2, foo: null});
+      expect(tableItems[2]).to.be.eql({[DEF_PK]: 3, foo: null});
+    });
+
+    it('allows to specify a short form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string1', bar: 10};
+      await adapter.create('model', input);
+      await adapter.create('model', input);
+      await adapter.create('model', input);
+      const result = await adapter.find('model', {fields: 'foo'});
+      expect(result).to.be.instanceof(Array);
+      expect(result).to.have.lengthOf(3);
+      expect(result[0]).to.be.eql({[DEF_PK]: 1, foo: input.foo});
+      expect(result[1]).to.be.eql({[DEF_PK]: 2, foo: input.foo});
+      expect(result[2]).to.be.eql({[DEF_PK]: 3, foo: input.foo});
+      const table = adapter._getTableOrCreate('model');
+      const tableItems = Array.from(table.values());
+      expect(tableItems).to.have.lengthOf(3);
+      expect(tableItems[0]).to.be.eql({[DEF_PK]: 1, ...input});
+      expect(tableItems[1]).to.be.eql({[DEF_PK]: 2, ...input});
+      expect(tableItems[2]).to.be.eql({[DEF_PK]: 3, ...input});
+    });
+
+    it('allows to specify a full form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+          baz: DataType.BOOLEAN,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string1', bar: 10, baz: true};
+      await adapter.create('model', input);
+      await adapter.create('model', input);
+      await adapter.create('model', input);
+      const result = await adapter.find('model', {fields: ['foo', 'bar']});
+      expect(result).to.be.instanceof(Array);
+      expect(result).to.have.lengthOf(3);
+      expect(result[0]).to.be.eql({
+        [DEF_PK]: 1,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      expect(result[1]).to.be.eql({
+        [DEF_PK]: 2,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      expect(result[2]).to.be.eql({
+        [DEF_PK]: 3,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      const table = adapter._getTableOrCreate('model');
+      const tableItems = Array.from(table.values());
+      expect(tableItems).to.have.lengthOf(3);
+      expect(tableItems[0]).to.be.eql({[DEF_PK]: 1, ...input});
+      expect(tableItems[1]).to.be.eql({[DEF_PK]: 2, ...input});
+      expect(tableItems[2]).to.be.eql({[DEF_PK]: 3, ...input});
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.BOOLEAN,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      await adapter.create('model', input);
+      await adapter.create('model', input);
+      await adapter.create('model', input);
+      const result = await adapter.find('model', {fields: ['foo', 'bar']});
+      expect(result).to.be.instanceof(Array);
+      expect(result).to.have.lengthOf(3);
+      expect(result[0]).to.be.eql({
+        [DEF_PK]: 1,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      expect(result[1]).to.be.eql({
+        [DEF_PK]: 2,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      expect(result[2]).to.be.eql({
+        [DEF_PK]: 3,
+        foo: input.foo,
+        bar: input.bar,
+      });
+      const table = adapter._getTableOrCreate('model');
+      const tableItems = Array.from(table.values());
+      const tableInput = {fooCol: 'string', barCol: 10, bazCol: true};
+      expect(tableItems).to.have.lengthOf(3);
+      expect(tableItems[0]).to.be.eql({[DEF_PK]: 1, ...tableInput});
+      expect(tableItems[1]).to.be.eql({[DEF_PK]: 2, ...tableInput});
+      expect(tableItems[2]).to.be.eql({[DEF_PK]: 3, ...tableInput});
+    });
+
+    it('allows to specify a short form of an order clause to sort a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {foo: 20});
+      await adapter.create('model', {foo: 10});
+      await adapter.create('model', {foo: 15});
+      const result1 = await adapter.find('model', {order: 'foo'});
+      const result2 = await adapter.find('model', {order: 'foo ASC'});
+      const result3 = await adapter.find('model', {order: 'foo DESC'});
+      expect(result1).to.have.lengthOf(3);
+      expect(result1[0]).to.be.eql({[DEF_PK]: 2, foo: 10});
+      expect(result1[1]).to.be.eql({[DEF_PK]: 3, foo: 15});
+      expect(result1[2]).to.be.eql({[DEF_PK]: 1, foo: 20});
+      expect(result2).to.have.lengthOf(3);
+      expect(result2[0]).to.be.eql({[DEF_PK]: 2, foo: 10});
+      expect(result2[1]).to.be.eql({[DEF_PK]: 3, foo: 15});
+      expect(result2[2]).to.be.eql({[DEF_PK]: 1, foo: 20});
+      expect(result3).to.have.lengthOf(3);
+      expect(result3[0]).to.be.eql({[DEF_PK]: 1, foo: 20});
+      expect(result3[1]).to.be.eql({[DEF_PK]: 3, foo: 15});
+      expect(result3[2]).to.be.eql({[DEF_PK]: 2, foo: 10});
+    });
+
+    it('allows to specify a full form of an order clause to sort a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {foo: 2, bar: 20});
+      await adapter.create('model', {foo: 2, bar: 10});
+      await adapter.create('model', {foo: 1, bar: 15});
+      const filter1 = {order: ['foo', 'bar']};
+      const filter2 = {order: ['foo ASC', 'bar ASC']};
+      const filter3 = {order: ['foo DESC', 'bar DESC']};
+      const filter4 = {order: ['foo', 'bar DESC']};
+      const result1 = await adapter.find('model', filter1);
+      const result2 = await adapter.find('model', filter2);
+      const result3 = await adapter.find('model', filter3);
+      const result4 = await adapter.find('model', filter4);
+      expect(result1).to.have.lengthOf(3);
+      expect(result1[0]).to.be.eql({[DEF_PK]: 3, foo: 1, bar: 15});
+      expect(result1[1]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
+      expect(result1[2]).to.be.eql({[DEF_PK]: 1, foo: 2, bar: 20});
+      expect(result2).to.have.lengthOf(3);
+      expect(result2[0]).to.be.eql({[DEF_PK]: 3, foo: 1, bar: 15});
+      expect(result2[1]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
+      expect(result2[2]).to.be.eql({[DEF_PK]: 1, foo: 2, bar: 20});
+      expect(result3).to.have.lengthOf(3);
+      expect(result3[0]).to.be.eql({[DEF_PK]: 1, foo: 2, bar: 20});
+      expect(result3[1]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
+      expect(result3[2]).to.be.eql({[DEF_PK]: 3, foo: 1, bar: 15});
+      expect(result4).to.have.lengthOf(3);
+      expect(result4[0]).to.be.eql({[DEF_PK]: 3, foo: 1, bar: 15});
+      expect(result4[1]).to.be.eql({[DEF_PK]: 1, foo: 2, bar: 20});
+      expect(result4[2]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
+    });
+
+    it('an order clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const tableInput1 = {fooCol: 2, barCol: 20};
+      const tableInput2 = {fooCol: 2, barCol: 10};
+      const tableInput3 = {fooCol: 1, barCol: 15};
+      table.set(1, {[DEF_PK]: 1, ...tableInput1});
+      table.set(2, {[DEF_PK]: 2, ...tableInput2});
+      table.set(3, {[DEF_PK]: 3, ...tableInput3});
+      const filter1 = {order: ['foo', 'bar']};
+      const filter2 = {order: ['foo ASC', 'bar ASC']};
+      const filter3 = {order: ['foo DESC', 'bar DESC']};
+      const filter4 = {order: ['foo', 'bar DESC']};
+      const result1 = await adapter.find('model', filter1);
+      const result2 = await adapter.find('model', filter2);
+      const result3 = await adapter.find('model', filter3);
+      const result4 = await adapter.find('model', filter4);
+      expect(result1).to.have.lengthOf(3);
+      expect(result1[0]).to.be.eql({[DEF_PK]: 3, foo: 1, bar: 15});
+      expect(result1[1]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
+      expect(result1[2]).to.be.eql({[DEF_PK]: 1, foo: 2, bar: 20});
+      expect(result2).to.have.lengthOf(3);
+      expect(result2[0]).to.be.eql({[DEF_PK]: 3, foo: 1, bar: 15});
+      expect(result2[1]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
+      expect(result2[2]).to.be.eql({[DEF_PK]: 1, foo: 2, bar: 20});
+      expect(result3).to.have.lengthOf(3);
+      expect(result3[0]).to.be.eql({[DEF_PK]: 1, foo: 2, bar: 20});
+      expect(result3[1]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
+      expect(result3[2]).to.be.eql({[DEF_PK]: 3, foo: 1, bar: 15});
+      expect(result4).to.have.lengthOf(3);
+      expect(result4[0]).to.be.eql({[DEF_PK]: 3, foo: 1, bar: 15});
+      expect(result4[1]).to.be.eql({[DEF_PK]: 1, foo: 2, bar: 20});
+      expect(result4[2]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
+    });
+
+    it('allows to specify a where clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+          bar: DataType.BOOLEAN,
+          baz: DataType.STRING,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input1 = {foo: 20, bar: true, baz: 'abc'};
+      const input2 = {foo: 10, bar: true, baz: 'abc'};
+      const input3 = {foo: 15, bar: false, baz: 'abe'};
+      await adapter.create('model', input1);
+      await adapter.create('model', input2);
+      await adapter.create('model', input3);
+      const filter1 = {where: {foo: 10}};
+      const filter2 = {where: {foo: {gte: 15}, baz: {like: 'bc'}}};
+      const filter3 = {where: {bar: true}};
+      const result1 = await adapter.find('model', filter1);
+      const result2 = await adapter.find('model', filter2);
+      const result3 = await adapter.find('model', filter3);
+      expect(result1).to.have.lengthOf(1);
+      expect(result1[0]).to.be.eql({[DEF_PK]: 2, ...input2});
+      expect(result2).to.have.lengthOf(1);
+      expect(result2[0]).to.be.eql({[DEF_PK]: 1, ...input1});
+      expect(result3).to.have.lengthOf(2);
+      expect(result3[0]).to.be.eql({[DEF_PK]: 1, ...input1});
+      expect(result3[1]).to.be.eql({[DEF_PK]: 2, ...input2});
+    });
+
+    it('a where clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.BOOLEAN,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const tableInput1 = {fooCol: 20, barCol: true, bazCol: 'abc'};
+      const tableInput2 = {fooCol: 10, barCol: true, bazCol: 'abc'};
+      const tableInput3 = {fooCol: 15, barCol: false, bazCol: 'abe'};
+      table.set(1, {[DEF_PK]: 1, ...tableInput1});
+      table.set(2, {[DEF_PK]: 2, ...tableInput2});
+      table.set(3, {[DEF_PK]: 3, ...tableInput3});
+      const input1 = {
+        foo: tableInput1.fooCol,
+        bar: tableInput1.barCol,
+        baz: tableInput1.bazCol,
+      };
+      const input2 = {
+        foo: tableInput2.fooCol,
+        bar: tableInput2.barCol,
+        baz: tableInput2.bazCol,
+      };
+      const filter1 = {where: {foo: 10}};
+      const filter2 = {where: {foo: {gte: 15}, baz: {like: 'bc'}}};
+      const filter3 = {where: {bar: true}};
+      const result1 = await adapter.find('model', filter1);
+      const result2 = await adapter.find('model', filter2);
+      const result3 = await adapter.find('model', filter3);
+      expect(result1).to.have.lengthOf(1);
+      expect(result1[0]).to.be.eql({[DEF_PK]: 2, ...input2});
+      expect(result2).to.have.lengthOf(1);
+      expect(result2[0]).to.be.eql({[DEF_PK]: 1, ...input1});
+      expect(result3).to.have.lengthOf(2);
+      expect(result3[0]).to.be.eql({[DEF_PK]: 1, ...input1});
+      expect(result3[1]).to.be.eql({[DEF_PK]: 2, ...input2});
+    });
+
+    it('a where clause uses a persisted data instead of default values', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'hello',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      table.set(1, {[DEF_PK]: 1, foo: undefined});
+      table.set(2, {[DEF_PK]: 2, foo: undefined});
+      table.set(3, {[DEF_PK]: 3, foo: 10});
+      const result1 = await adapter.find('model', {where: {foo: undefined}});
+      const result2 = await adapter.find('model', {where: {foo: 10}});
+      const result3 = await adapter.find('model', {where: {foo: 'hello'}});
+      expect(table.size).to.be.eq(3);
+      expect(result1).to.have.lengthOf(2);
+      expect(result1[0]).to.be.eql({[DEF_PK]: 1, foo: 'hello'});
+      expect(result1[1]).to.be.eql({[DEF_PK]: 2, foo: 'hello'});
+      expect(result2).to.have.lengthOf(1);
+      expect(result2[0]).to.be.eql({[DEF_PK]: 3, foo: 10});
+      expect(result3).to.be.empty;
+    });
+
+    it('allows to specify a limit clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {});
+      await adapter.create('model', {});
+      await adapter.create('model', {});
+      const result1 = await adapter.find('model', {limit: 0});
+      const result2 = await adapter.find('model', {limit: 1});
+      const result3 = await adapter.find('model', {limit: 2});
+      expect(result1).to.have.lengthOf(3);
+      expect(result1[0]).to.be.eql({[DEF_PK]: 1});
+      expect(result1[1]).to.be.eql({[DEF_PK]: 2});
+      expect(result1[2]).to.be.eql({[DEF_PK]: 3});
+      expect(result2).to.have.lengthOf(1);
+      expect(result2[0]).to.be.eql({[DEF_PK]: 1});
+      expect(result3).to.have.lengthOf(2);
+      expect(result3[0]).to.be.eql({[DEF_PK]: 1});
+      expect(result3[1]).to.be.eql({[DEF_PK]: 2});
+    });
+
+    it('allows to specify a skip clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {});
+      await adapter.create('model', {});
+      await adapter.create('model', {});
+      const result1 = await adapter.find('model', {skip: 0});
+      const result2 = await adapter.find('model', {skip: 1});
+      const result3 = await adapter.find('model', {skip: 2});
+      expect(result1).to.have.lengthOf(3);
+      expect(result1[0]).to.be.eql({[DEF_PK]: 1});
+      expect(result1[1]).to.be.eql({[DEF_PK]: 2});
+      expect(result1[2]).to.be.eql({[DEF_PK]: 3});
+      expect(result2).to.have.lengthOf(2);
+      expect(result1[1]).to.be.eql({[DEF_PK]: 2});
+      expect(result1[2]).to.be.eql({[DEF_PK]: 3});
+      expect(result3).to.have.lengthOf(1);
+      expect(result1[2]).to.be.eql({[DEF_PK]: 3});
+    });
+  });
+
+  describe('findById', function () {
+    it('throws an error if a given identifier does not exist', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const promise = adapter.findById('model', 1);
+      await expect(promise).to.be.rejectedWith(
+        format(
+          'The value 1 of the primary key "%s" does not exist in the model "model".',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('uses default values for non-existent properties', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const idValue = 1;
+      table.set(idValue, {[DEF_PK]: idValue});
+      const result = await adapter.findById('model', idValue);
+      expect(result).to.be.eql({[DEF_PK]: idValue, foo: 'string'});
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue});
+    });
+
+    it('uses default values for properties of an undefined', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const idValue = 1;
+      const input = {foo: undefined};
+      table.set(idValue, {[DEF_PK]: idValue, ...input});
+      const result = await adapter.findById('model', idValue);
+      expect(result).to.be.eql({[DEF_PK]: idValue, foo: 'string'});
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('uses default values for properties of a null', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const idValue = 1;
+      const input = {foo: null};
+      table.set(idValue, {[DEF_PK]: idValue, ...input});
+      const result = await adapter.findById('model', idValue);
+      expect(result).to.be.eql({[DEF_PK]: idValue, foo: 'string'});
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('uses a specified column name for a primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            columnName: 'qux',
+          },
+          bar: DataType.NUMBER,
+          baz: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const input = {bar: 1, baz: 2};
+      const table = adapter._getTableOrCreate('model');
+      const idValue = 1;
+      table.set(idValue, {qux: idValue, ...input});
+      const result = await adapter.findById('model', idValue);
+      expect(result).to.be.eql({foo: idValue, ...input});
+    });
+
+    it('uses a specified column name for a regular property', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'baz',
+          },
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const idValue = 1;
+      const input = {foo: 1, bar: 2};
+      table.set(idValue, {
+        [DEF_PK]: idValue,
+        baz: input.foo,
+        bar: input.bar,
+      });
+      const result = await adapter.findById('model', idValue);
+      expect(result).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('uses a specified column name for a regular property with a default value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'baz',
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const idValue = 1;
+      table.set(idValue, {[DEF_PK]: idValue});
+      const defaults = {foo: 1, bar: 2};
+      const result = await adapter.findById('model', idValue);
+      expect(result).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('allows to specify a short form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const input = {foo: 'string', bar: 10};
+      const idValue = 1;
+      table.set(idValue, {[DEF_PK]: idValue, ...input});
+      const result = await adapter.findById('model', idValue, {fields: 'foo'});
+      expect(result).to.be.eql({[DEF_PK]: idValue, foo: input.foo});
+    });
+
+    it('allows to specify a full form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+          baz: DataType.BOOLEAN,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const input = {foo: 'string', bar: 10, baz: true};
+      const idValue = 1;
+      table.set(idValue, {[DEF_PK]: idValue, ...input});
+      const filter = {fields: ['foo', 'bar']};
+      const result = await adapter.findById('model', idValue, filter);
+      expect(result).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: input.foo,
+        bar: input.bar,
+      });
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.BOOLEAN,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const tableInput = {fooCol: 'string', barCol: 10, bazCol: true};
+      const idValue = 1;
+      table.set(idValue, {
+        [DEF_PK]: idValue,
+        ...tableInput,
+      });
+      const filter = {fields: ['foo', 'bar']};
+      const result = await adapter.findById('model', idValue, filter);
+      expect(result).to.be.eql({
+        [DEF_PK]: idValue,
+        foo: tableInput.fooCol,
+        bar: tableInput.barCol,
+      });
+    });
+  });
+
+  describe('delete', function () {
+    it('removes all table items and returns their number', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      table.set(1, {[DEF_PK]: 1});
+      table.set(2, {[DEF_PK]: 2});
+      table.set(3, {[DEF_PK]: 3});
+      const result = await adapter.delete('model');
+      expect(result).to.be.eq(3);
+      const tableSize = Array.from(table.values()).length;
+      expect(tableSize).to.be.eq(0);
+    });
+
+    it('returns zero if nothing to remove', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result = await adapter.delete('model');
+      expect(result).to.be.eq(0);
+    });
+
+    it('uses a given where clause to remove specific items', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {foo: 20});
+      await adapter.create('model', {foo: 10});
+      await adapter.create('model', {foo: 15});
+      const result = await adapter.delete('model', {foo: {gte: 15}});
+      expect(result).to.be.eq(2);
+      const table = adapter._getTableOrCreate('model');
+      const tableSize = Array.from(table.values()).length;
+      expect(tableSize).to.be.eq(1);
+      expect(table.get(2)).to.be.eql({[DEF_PK]: 2, foo: 10});
+    });
+
+    it('a where clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'fooCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      table.set(1, {[DEF_PK]: 1, fooCol: 20});
+      table.set(2, {[DEF_PK]: 2, fooCol: 10});
+      table.set(3, {[DEF_PK]: 3, fooCol: 15});
+      const result = await adapter.delete('model', {foo: {gte: 15}});
+      expect(result).to.be.eq(2);
+      expect(table.size).to.be.eq(1);
+      expect(table.get(2)).to.be.eql({[DEF_PK]: 2, fooCol: 10});
+    });
+
+    it('a where clause uses a persisted data instead of default values', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'hello',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const input1 = {[DEF_PK]: 1, foo: undefined};
+      const input2 = {[DEF_PK]: 2, foo: undefined};
+      const input3 = {[DEF_PK]: 3, foo: 10};
+      const input4 = {[DEF_PK]: 4, foo: true};
+      table.set(1, input1);
+      table.set(2, input2);
+      table.set(3, input3);
+      table.set(4, input4);
+      const result1 = await adapter.delete('model', {foo: undefined});
+      expect(result1).to.be.eq(2);
+      expect(table.size).to.be.eq(2);
+      expect(table.get(3)).to.be.eql(input3);
+      expect(table.get(4)).to.be.eql(input4);
+      const result2 = await adapter.delete('model', {foo: 'hello'});
+      expect(result2).to.be.eq(0);
+      expect(table.size).to.be.eq(2);
+      expect(table.get(3)).to.be.eql(input3);
+      expect(table.get(4)).to.be.eql(input4);
+      const result3 = await adapter.delete('model', {foo: 10});
+      expect(result3).to.be.eq(1);
+      expect(table.size).to.be.eq(1);
+      expect(table.get(4)).to.be.eql(input4);
+    });
+  });
+
+  describe('deleteById', function () {
+    it('returns false if a given identifier is not exist', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result = await adapter.deleteById('model', 1);
+      expect(result).to.be.false;
+    });
+
+    it('returns true if an item has removed by a given identifier', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      const result = await adapter.deleteById('model', idValue);
+      expect(result).to.be.true;
+    });
+
+    it('uses a specified column name for a primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            columnName: 'qux',
+          },
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const idValue = 1;
+      table.set(idValue, {qux: idValue, bar: 10});
+      const result = await adapter.deleteById('model', idValue);
+      expect(result).to.be.true;
+      const tableSize = Array.from(table.values()).length;
+      expect(tableSize).to.be.eq(0);
+    });
+  });
+
+  describe('exists', function () {
+    it('returns false if a given identifier is not exist', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result = await adapter.exists('model', 1);
+      expect(result).to.be.false;
+    });
+
+    it('returns true if a given identifier is exist', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {});
+      const result = await adapter.exists('model', 1);
+      expect(result).to.be.true;
+    });
+
+    it('uses a specified column name for a primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            columnName: 'qux',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      const idValue = 1;
+      table.set(idValue, {qux: idValue});
+      const result = await adapter.exists('model', idValue);
+      expect(result).to.be.true;
+    });
+  });
+
+  describe('count', function () {
+    it('returns zero if nothing to count', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const result = await adapter.count('model');
+      expect(result).to.be.eq(0);
+    });
+
+    it('returns zero if a given where clause does not met', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {foo: 20});
+      await adapter.create('model', {foo: 10});
+      await adapter.create('model', {foo: 15});
+      const result = await adapter.count('model', {foo: {gte: 150}});
+      expect(result).to.be.eq(0);
+      const table = adapter._getTableOrCreate('model');
+      const tableSize = Array.from(table.values()).length;
+      expect(tableSize).to.be.eq(3);
+      expect(table.get(1)).to.be.eql({[DEF_PK]: 1, foo: 20});
+      expect(table.get(2)).to.be.eql({[DEF_PK]: 2, foo: 10});
+      expect(table.get(3)).to.be.eql({[DEF_PK]: 3, foo: 15});
+    });
+
+    it('returns a number of table items', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {});
+      await adapter.create('model', {});
+      await adapter.create('model', {});
+      const result = await adapter.count('model');
+      expect(result).to.be.eq(3);
+    });
+
+    it('uses a given where clause to count specific items', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      await adapter.create('model', {foo: 20});
+      await adapter.create('model', {foo: 10});
+      await adapter.create('model', {foo: 15});
+      const result = await adapter.count('model', {foo: {gte: 15}});
+      expect(result).to.be.eq(2);
+      const table = adapter._getTableOrCreate('model');
+      const tableSize = Array.from(table.values()).length;
+      expect(tableSize).to.be.eq(3);
+      expect(table.get(1)).to.be.eql({[DEF_PK]: 1, foo: 20});
+      expect(table.get(2)).to.be.eql({[DEF_PK]: 2, foo: 10});
+      expect(table.get(3)).to.be.eql({[DEF_PK]: 3, foo: 15});
+    });
+
+    it('a where clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'fooCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      table.set(1, {[DEF_PK]: 1, fooCol: 20});
+      table.set(2, {[DEF_PK]: 2, fooCol: 10});
+      table.set(3, {[DEF_PK]: 3, fooCol: 15});
+      const result = await adapter.count('model', {foo: {gte: 15}});
+      expect(result).to.be.eq(2);
+      const tableSize = Array.from(table.values()).length;
+      expect(tableSize).to.be.eq(3);
+      expect(table.get(1)).to.be.eql({[DEF_PK]: 1, fooCol: 20});
+      expect(table.get(2)).to.be.eql({[DEF_PK]: 2, fooCol: 10});
+      expect(table.get(3)).to.be.eql({[DEF_PK]: 3, fooCol: 15});
+    });
+
+    it('a where clause uses a persisted data instead of default values', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'hello',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema._services, {});
+      const table = adapter._getTableOrCreate('model');
+      table.set(1, {[DEF_PK]: 1, foo: undefined});
+      table.set(2, {[DEF_PK]: 2, foo: undefined});
+      table.set(3, {[DEF_PK]: 3, foo: 10});
+      const result1 = await adapter.count('model', {foo: undefined});
+      const result2 = await adapter.count('model', {foo: 10});
+      const result3 = await adapter.count('model', {foo: 'hello'});
+      expect(result1).to.be.eq(2);
+      expect(result2).to.be.eq(1);
+      expect(result3).to.be.eq(0);
+    });
+  });
+});

+ 44 - 0
src/adapter/decorator/data-sanitizing-decorator.js

@@ -0,0 +1,44 @@
+import {Adapter} from '../adapter.js';
+import {Service} from '../../service/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {ModelDataSanitizer} from '../../definition/index.js';
+
+/**
+ * Data sanitizing decorator.
+ */
+export class DataSanitizingDecorator extends Service {
+  /**
+   * Decorate.
+   *
+   * @param adapter
+   */
+  decorate(adapter) {
+    if (!adapter || !(adapter instanceof Adapter))
+      throw new InvalidArgumentError(
+        'A first argument of DataSanitizingDecorator.decorate must be ' +
+          'an Adapter instance, but %s given.',
+        adapter,
+      );
+
+    const sanitizer = adapter.get(ModelDataSanitizer);
+    const sanitize = (...args) => sanitizer.sanitize(...args);
+
+    const create = adapter.create;
+    adapter.create = async function (modelName, modelData, filter) {
+      modelData = sanitize(modelName, modelData);
+      return create.call(this, modelName, modelData, filter);
+    };
+
+    const replaceById = adapter.replaceById;
+    adapter.replaceById = async function (modelName, id, modelData, filter) {
+      modelData = sanitize(modelName, modelData);
+      return replaceById.call(this, modelName, id, modelData, filter);
+    };
+
+    const patchById = adapter.patchById;
+    adapter.patchById = async function (modelName, id, modelData, filter) {
+      modelData = sanitize(modelName, modelData);
+      return patchById.call(this, modelName, id, modelData, filter);
+    };
+  }
+}

+ 59 - 0
src/adapter/decorator/data-sanitizing-decorator.spec.js

@@ -0,0 +1,59 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {Adapter} from '../adapter.js';
+import {Schema} from '../../schema.js';
+import {ModelDataSanitizer} from '../../definition/index.js';
+
+const S = new Schema();
+S.defineModel({name: 'model'});
+
+class TestAdapter extends Adapter {
+  // eslint-disable-next-line no-unused-vars
+  create(modelName, modelData, filter = undefined) {
+    return Promise.resolve({});
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  replaceById(modelName, id, modelData, filter = undefined) {
+    return Promise.resolve({});
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  patchById(modelName, id, modelData, filter = undefined) {
+    return Promise.resolve({});
+  }
+}
+
+const A = S.get(TestAdapter);
+const V = S.get(ModelDataSanitizer);
+const sandbox = chai.spy.sandbox();
+
+describe('DataSanitizingDecorator', function () {
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  it('overrides the "create" method and sanitizes a given data', async function () {
+    sandbox.on(V, 'sanitize');
+    const data = {};
+    await A.create('model', data);
+    expect(V.sanitize).to.be.called.once;
+    expect(V.sanitize).to.be.called.with.exactly('model', data);
+  });
+
+  it('overrides the "replaceById" method and sanitizes a given data', async function () {
+    sandbox.on(V, 'sanitize');
+    const data = {};
+    await A.replaceById('model', 1, data);
+    expect(V.sanitize).to.be.called.once;
+    expect(V.sanitize).to.be.called.with.exactly('model', data);
+  });
+
+  it('overrides the "patchById" method and sanitizes a given data', async function () {
+    sandbox.on(V, 'sanitize');
+    const data = {};
+    await A.patchById('model', 1, data);
+    expect(V.sanitize).to.be.called.once;
+    expect(V.sanitize).to.be.called.with.exactly('model', data);
+  });
+});

+ 41 - 0
src/adapter/decorator/data-validation-decorator.js

@@ -0,0 +1,41 @@
+import {Adapter} from '../adapter.js';
+import {Service} from '../../service/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {ModelDataValidator} from '../../definition/index.js';
+
+/**
+ * Data validation decorator.
+ */
+export class DataValidationDecorator extends Service {
+  /**
+   * Decorate.
+   *
+   * @param {Adapter} adapter
+   */
+  decorate(adapter) {
+    if (!adapter || !(adapter instanceof Adapter))
+      throw new InvalidArgumentError(
+        'A first argument of DataValidationDecorator.decorate must be ' +
+          'an Adapter instance, but %s given.',
+        adapter,
+      );
+
+    const create = adapter.create;
+    adapter.create = function (modelName, modelData, filter) {
+      this.get(ModelDataValidator).validate(modelName, modelData);
+      return create.call(this, modelName, modelData, filter);
+    };
+
+    const replaceById = adapter.replaceById;
+    adapter.replaceById = function (modelName, id, modelData, filter) {
+      this.get(ModelDataValidator).validate(modelName, modelData);
+      return replaceById.call(this, modelName, id, modelData, filter);
+    };
+
+    const patchById = adapter.patchById;
+    adapter.patchById = function (modelName, id, modelData, filter) {
+      this.get(ModelDataValidator).validate(modelName, modelData, true);
+      return patchById.call(this, modelName, id, modelData, filter);
+    };
+  }
+}

+ 59 - 0
src/adapter/decorator/data-validation-decorator.spec.js

@@ -0,0 +1,59 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {Adapter} from '../adapter.js';
+import {Schema} from '../../schema.js';
+import {ModelDataValidator} from '../../definition/index.js';
+
+const S = new Schema();
+S.defineModel({name: 'model'});
+
+class TestAdapter extends Adapter {
+  // eslint-disable-next-line no-unused-vars
+  create(modelName, modelData, filter = undefined) {
+    return Promise.resolve({});
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  replaceById(modelName, id, modelData, filter = undefined) {
+    return Promise.resolve({});
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  patchById(modelName, id, modelData, filter = undefined) {
+    return Promise.resolve({});
+  }
+}
+
+const A = S.get(TestAdapter);
+const V = S.get(ModelDataValidator);
+const sandbox = chai.spy.sandbox();
+
+describe('DataValidationDecorator', function () {
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  it('overrides the "create" method and validates a given data', async function () {
+    sandbox.on(V, 'validate');
+    const data = {};
+    await A.create('model', data);
+    expect(V.validate).to.be.called.once;
+    expect(V.validate).to.be.called.with.exactly('model', data);
+  });
+
+  it('overrides the "replaceById" method and validates a given data', async function () {
+    sandbox.on(V, 'validate');
+    const data = {};
+    await A.replaceById('model', 1, data);
+    expect(V.validate).to.be.called.once;
+    expect(V.validate).to.be.called.with.exactly('model', data);
+  });
+
+  it('overrides the "patchById" method and validates a given data', async function () {
+    sandbox.on(V, 'validate');
+    const data = {};
+    await A.patchById('model', 1, data);
+    expect(V.validate).to.be.called.once;
+    expect(V.validate).to.be.called.with.exactly('model', data, true);
+  });
+});

+ 57 - 0
src/adapter/decorator/default-values-decorator.js

@@ -0,0 +1,57 @@
+import {Adapter} from '../adapter.js';
+import {Service} from '../../service/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {ModelDefinitionUtils} from '../../definition/index.js';
+
+/**
+ * Default values decorator.
+ */
+export class DefaultValuesDecorator extends Service {
+  /**
+   * Decorate.
+   *
+   * @param {Adapter} adapter
+   */
+  decorate(adapter) {
+    if (!adapter || !(adapter instanceof Adapter))
+      throw new InvalidArgumentError(
+        'A first argument of DefaultValuesDecorator.decorate must be ' +
+          'an Adapter instance, but %s given.',
+        adapter,
+      );
+
+    const utils = adapter.get(ModelDefinitionUtils);
+    const setDefaults = (...args) =>
+      utils.setDefaultValuesToEmptyProperties(...args);
+
+    const create = adapter.create;
+    adapter.create = function (modelName, modelData, filter) {
+      modelData = setDefaults(modelName, modelData);
+      return create.call(this, modelName, modelData, filter);
+    };
+
+    const replaceById = adapter.replaceById;
+    adapter.replaceById = function (modelName, id, modelData, filter) {
+      modelData = setDefaults(modelName, modelData);
+      return replaceById.call(this, modelName, id, modelData, filter);
+    };
+
+    const patchById = adapter.patchById;
+    adapter.patchById = function (modelName, id, modelData, filter) {
+      modelData = setDefaults(modelName, modelData, true);
+      return patchById.call(this, modelName, id, modelData, filter);
+    };
+
+    const find = adapter.find;
+    adapter.find = async function (modelName, filter) {
+      const modelItems = await find.call(this, modelName, filter);
+      return modelItems.map(modelItem => setDefaults(modelName, modelItem));
+    };
+
+    const findById = adapter.findById;
+    adapter.findById = async function (modelName, id, filter) {
+      const retvalData = await findById.call(this, modelName, id, filter);
+      return setDefaults(modelName, retvalData);
+    };
+  }
+}

+ 141 - 0
src/adapter/decorator/default-values-decorator.spec.js

@@ -0,0 +1,141 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {Adapter} from '../adapter.js';
+import {Schema} from '../../schema.js';
+import {DataType} from '../../definition/index.js';
+import {ModelDefinitionUtils} from '../../definition/index.js';
+
+const S = new Schema();
+S.defineModel({
+  name: 'model',
+  properties: {
+    prop: {
+      type: DataType.STRING,
+      default: 'value',
+    },
+  },
+});
+
+const INPUT_DATA = {};
+
+class TestAdapter extends Adapter {
+  // eslint-disable-next-line no-unused-vars
+  async create(modelName, modelData, filter = undefined) {
+    return modelData;
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async replaceById(modelName, id, modelData, filter = undefined) {
+    return modelData;
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async patchById(modelName, id, modelData, filter = undefined) {
+    return modelData;
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async find(modelName, filter = undefined) {
+    return [INPUT_DATA];
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async findById(modelName, id, filter = undefined) {
+    return INPUT_DATA;
+  }
+}
+
+const A = S.get(TestAdapter);
+const U = S.get(ModelDefinitionUtils);
+const sandbox = chai.spy.sandbox();
+
+describe('DefaultValuesDecorator', function () {
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  it('overrides the "create" method method and sets default values to input data', async function () {
+    sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+    const retval = await A.create('model', INPUT_DATA);
+    expect(retval).to.be.eql({prop: 'value'});
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+      'model',
+      INPUT_DATA,
+    );
+  });
+
+  it('overrides the "replaceById" method and sets default values to input data', async function () {
+    sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+    const retval = await A.replaceById('model', 1, INPUT_DATA);
+    expect(retval).to.be.eql({prop: 'value'});
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+      'model',
+      INPUT_DATA,
+    );
+  });
+
+  describe('overrides the "patchById" method and sets default values to input data', function () {
+    it('does not set default values to not existing properties of input data', async function () {
+      sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+      const data = {};
+      const retval = await A.patchById('model', 1, data);
+      expect(retval).to.be.eql({});
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+        'model',
+        data,
+        true,
+      );
+    });
+
+    it('does set default values to input properties of null', async function () {
+      sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+      const data = {prop: null};
+      const retval = await A.patchById('model', 2, data);
+      expect(retval).to.be.eql({prop: 'value'});
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+        'model',
+        data,
+        true,
+      );
+    });
+
+    it('does set default values to input properties of undefined', async function () {
+      sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+      const data = {prop: undefined};
+      const retval = await A.patchById('model', 3, data);
+      expect(retval).to.be.eql({prop: 'value'});
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+        'model',
+        data,
+        true,
+      );
+    });
+  });
+
+  it('overrides the "find" method and sets default values to output data', async function () {
+    sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+    const retval = await A.find('model');
+    expect(retval).to.be.eql([{prop: 'value'}]);
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+      'model',
+      INPUT_DATA,
+    );
+  });
+
+  it('overrides the "findById" method and sets default values to output data', async function () {
+    sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+    const retval = await A.findById('model', 1);
+    expect(retval).to.be.eql({prop: 'value'});
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+      'model',
+      INPUT_DATA,
+    );
+  });
+});

+ 72 - 0
src/adapter/decorator/fields-filtering-decorator.js

@@ -0,0 +1,72 @@
+import {Adapter} from '../adapter.js';
+import {Service} from '../../service/index.js';
+import {FieldsClauseTool} from '../../filter/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+
+/**
+ * Fields filtering decorator.
+ */
+export class FieldsFilteringDecorator extends Service {
+  /**
+   * Decorate.
+   *
+   * @param adapter
+   */
+  decorate(adapter) {
+    if (!adapter || !(adapter instanceof Adapter))
+      throw new InvalidArgumentError(
+        'A first argument of FieldsFilteringDecorator.decorate must be ' +
+          'an Adapter instance, but %s given.',
+        adapter,
+      );
+
+    const tool = adapter.get(FieldsClauseTool);
+    const selectFields = (...args) => tool.filter(...args);
+
+    const create = adapter.create;
+    adapter.create = async function (modelName, modelData, filter) {
+      let result = await create.call(this, modelName, modelData, filter);
+      if (filter && typeof filter === 'object' && filter.fields)
+        result = selectFields(result, modelName, filter.fields);
+      return result;
+    };
+
+    const replaceById = adapter.replaceById;
+    adapter.replaceById = async function (modelName, id, modelData, filter) {
+      let result = await replaceById.call(
+        this,
+        modelName,
+        id,
+        modelData,
+        filter,
+      );
+      if (filter && typeof filter === 'object' && filter.fields)
+        result = selectFields(result, modelName, filter.fields);
+      return result;
+    };
+
+    const patchById = adapter.patchById;
+    adapter.patchById = async function (modelName, id, modelData, filter) {
+      let result = await patchById.call(this, modelName, id, modelData, filter);
+      if (filter && typeof filter === 'object' && filter.fields)
+        result = selectFields(result, modelName, filter.fields);
+      return result;
+    };
+
+    const find = adapter.find;
+    adapter.find = async function (modelName, filter) {
+      let result = await find.call(this, modelName, filter);
+      if (filter && typeof filter === 'object' && filter.fields)
+        result = selectFields(result, modelName, filter.fields);
+      return result;
+    };
+
+    const findById = adapter.findById;
+    adapter.findById = async function (modelName, id, filter) {
+      let result = await findById.call(this, modelName, id, filter);
+      if (filter && typeof filter === 'object' && filter.fields)
+        result = selectFields(result, modelName, filter.fields);
+      return result;
+    };
+  }
+}

+ 119 - 0
src/adapter/decorator/fields-filtering-decorator.spec.js

@@ -0,0 +1,119 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {Adapter} from '../adapter.js';
+import {Schema} from '../../schema.js';
+import {FieldsClauseTool} from '../../filter/index.js';
+
+const S = new Schema();
+const MODEL_NAME = 'model';
+S.defineModel({name: MODEL_NAME});
+
+const FILTER = {fields: ['foo', 'bar']};
+const MODEL_DATA = {
+  foo: 'fooVal',
+  bar: 'barVal',
+  baz: 'bazVal',
+  qux: 'quxVal',
+};
+
+const RETVAL_DATA = {
+  foo: MODEL_DATA.foo,
+  bar: MODEL_DATA.bar,
+};
+
+class TestAdapter extends Adapter {
+  // eslint-disable-next-line no-unused-vars
+  async create(modelName, modelData, filter = undefined) {
+    return MODEL_DATA;
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async replaceById(modelName, id, modelData, filter = undefined) {
+    return MODEL_DATA;
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async patchById(modelName, id, modelData, filter = undefined) {
+    return MODEL_DATA;
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async find(modelName, filter = undefined) {
+    return [MODEL_DATA];
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async findById(modelName, id, filter = undefined) {
+    return MODEL_DATA;
+  }
+}
+
+const A = S.get(TestAdapter);
+const T = S.get(FieldsClauseTool);
+const sandbox = chai.spy.sandbox();
+
+describe('FieldsFilteringDecorator', function () {
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  it('overrides the "create" method method and filtering output fields', async function () {
+    sandbox.on(T, 'filter');
+    const retval = await A.create(MODEL_NAME, {}, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.filter).to.be.called.once;
+    expect(T.filter).to.be.called.with.exactly(
+      MODEL_DATA,
+      MODEL_NAME,
+      FILTER.fields,
+    );
+  });
+
+  it('overrides the "replaceById" method and filtering output fields', async function () {
+    sandbox.on(T, 'filter');
+    const retval = await A.replaceById(MODEL_NAME, 1, {}, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.filter).to.be.called.once;
+    expect(T.filter).to.be.called.with.exactly(
+      MODEL_DATA,
+      MODEL_NAME,
+      FILTER.fields,
+    );
+  });
+
+  it('overrides the "patchById" method and filtering output fields', async function () {
+    sandbox.on(T, 'filter');
+    const retval = await A.patchById(MODEL_NAME, 1, {}, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.filter).to.be.called.once;
+    expect(T.filter).to.be.called.with.exactly(
+      MODEL_DATA,
+      MODEL_NAME,
+      FILTER.fields,
+    );
+  });
+
+  it('overrides the "find" method and filtering output fields', async function () {
+    sandbox.on(T, 'filter', function (entities, modelName, fields) {
+      expect(entities).to.be.eql([MODEL_DATA]);
+      expect(modelName).to.be.eq(MODEL_NAME);
+      expect(fields).to.be.eql(FILTER.fields);
+      return [RETVAL_DATA];
+    });
+    const retval = await A.find(MODEL_NAME, FILTER);
+    expect(retval).to.be.eql([RETVAL_DATA]);
+    expect(T.filter).to.be.called.once;
+  });
+
+  it('overrides the "findById" method and filtering output fields', async function () {
+    sandbox.on(T, 'filter');
+    const retval = await A.findById(MODEL_NAME, 1, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.filter).to.be.called.once;
+    expect(T.filter).to.be.called.with.exactly(
+      MODEL_DATA,
+      MODEL_NAME,
+      FILTER.fields,
+    );
+  });
+});

+ 78 - 0
src/adapter/decorator/inclusion-decorator.js

@@ -0,0 +1,78 @@
+import {Adapter} from '../adapter.js';
+import {Service} from '../../service/index.js';
+import {IncludeClauseTool} from '../../filter/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+
+/**
+ * Inclusion decorator.
+ */
+export class InclusionDecorator extends Service {
+  /**
+   * Decorate.
+   *
+   * @param {Adapter} adapter
+   */
+  decorate(adapter) {
+    if (!adapter || !(adapter instanceof Adapter))
+      throw new InvalidArgumentError(
+        'A first argument of InclusionDecorator.decorate must be ' +
+          'an Adapter instance, but %s given.',
+        adapter,
+      );
+
+    const tool = adapter.get(IncludeClauseTool);
+    const includeTo = (...args) => tool.includeTo(...args);
+
+    const create = adapter.create;
+    adapter.create = async function (modelName, modelData, filter) {
+      const retvalData = await create.call(this, modelName, modelData, filter);
+      if (filter && typeof filter === 'object' && filter.include)
+        await includeTo([retvalData], modelName, filter.include);
+      return retvalData;
+    };
+
+    const replaceById = adapter.replaceById;
+    adapter.replaceById = async function (modelName, id, modelData, filter) {
+      const retvalData = await replaceById.call(
+        this,
+        modelName,
+        id,
+        modelData,
+        filter,
+      );
+      if (filter && typeof filter === 'object' && filter.include)
+        await includeTo([retvalData], modelName, filter.include);
+      return retvalData;
+    };
+
+    const patchById = adapter.patchById;
+    adapter.patchById = async function (modelName, id, modelData, filter) {
+      const retvalData = await patchById.call(
+        this,
+        modelName,
+        id,
+        modelData,
+        filter,
+      );
+      if (filter && typeof filter === 'object' && filter.include)
+        await includeTo([retvalData], modelName, filter.include);
+      return retvalData;
+    };
+
+    const find = adapter.find;
+    adapter.find = async function (modelName, filter) {
+      const modelItems = await find.call(this, modelName, filter);
+      if (filter && typeof filter === 'object' && filter.include)
+        await includeTo(modelItems, modelName, filter.include);
+      return modelItems;
+    };
+
+    const findById = adapter.findById;
+    adapter.findById = async function (modelName, id, filter) {
+      const retvalData = await findById.call(this, modelName, id, filter);
+      if (filter && typeof filter === 'object' && filter.include)
+        await includeTo([retvalData], modelName, filter.include);
+      return retvalData;
+    };
+  }
+}

+ 117 - 0
src/adapter/decorator/inclusion-decorator.spec.js

@@ -0,0 +1,117 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {Adapter} from '../adapter.js';
+import {Schema} from '../../schema.js';
+import {IncludeClauseTool} from '../../filter/index.js';
+
+const S = new Schema();
+S.defineModel({name: 'model'});
+const FILTER = {include: 'parent'};
+
+const MODEL_DATA = {
+  foo: 'fooVal',
+  bar: 'barVal',
+};
+
+const RETVAL_DATA = {
+  foo: MODEL_DATA.foo,
+  bar: MODEL_DATA.bar,
+  baz: 'bazVal',
+};
+
+class TestAdapter extends Adapter {
+  // eslint-disable-next-line no-unused-vars
+  async create(modelName, modelData, filter = undefined) {
+    return Object.assign({}, MODEL_DATA);
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async replaceById(modelName, id, modelData, filter = undefined) {
+    return Object.assign({}, MODEL_DATA);
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async patchById(modelName, id, modelData, filter = undefined) {
+    return Object.assign({}, MODEL_DATA);
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async find(modelName, filter = undefined) {
+    return [Object.assign({}, MODEL_DATA)];
+  }
+
+  // eslint-disable-next-line no-unused-vars
+  async findById(modelName, id, filter = undefined) {
+    return Object.assign({}, MODEL_DATA);
+  }
+}
+
+const A = S.get(TestAdapter);
+const T = S.get(IncludeClauseTool);
+const sandbox = chai.spy.sandbox();
+
+describe('InclusionDecorator', function () {
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  it('overrides the "create" method method and applies clause inclusion', async function () {
+    sandbox.on(T, 'includeTo', function (entities, modelName, clause) {
+      expect(entities).to.be.eql([MODEL_DATA]);
+      expect(modelName).to.be.eql('model');
+      expect(clause).to.be.eql(FILTER.include);
+      Object.assign(entities[0], RETVAL_DATA);
+    });
+    const retval = await A.create('model', {}, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.includeTo).to.be.called.once;
+  });
+
+  it('overrides the "replaceById" method and applies clause inclusion', async function () {
+    sandbox.on(T, 'includeTo', function (entities, modelName, clause) {
+      expect(entities).to.be.eql([MODEL_DATA]);
+      expect(modelName).to.be.eql('model');
+      expect(clause).to.be.eql(FILTER.include);
+      Object.assign(entities[0], RETVAL_DATA);
+    });
+    const retval = await A.replaceById('model', 1, {}, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.includeTo).to.be.called.once;
+  });
+
+  it('overrides the "patchById" method and applies clause inclusion', async function () {
+    sandbox.on(T, 'includeTo', function (entities, modelName, clause) {
+      expect(entities).to.be.eql([MODEL_DATA]);
+      expect(modelName).to.be.eql('model');
+      expect(clause).to.be.eql(FILTER.include);
+      Object.assign(entities[0], RETVAL_DATA);
+    });
+    const retval = await A.patchById('model', 1, {}, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.includeTo).to.be.called.once;
+  });
+
+  it('overrides the "find" method and applies clause inclusion', async function () {
+    sandbox.on(T, 'includeTo', function (entities, modelName, clause) {
+      expect(entities).to.be.eql([MODEL_DATA]);
+      expect(modelName).to.be.eql('model');
+      expect(clause).to.be.eql(FILTER.include);
+      Object.assign(entities[0], RETVAL_DATA);
+    });
+    const retval = await A.find('model', FILTER);
+    expect(retval).to.be.eql([RETVAL_DATA]);
+    expect(T.includeTo).to.be.called.once;
+  });
+
+  it('overrides the "findById" method and applies clause inclusion', async function () {
+    sandbox.on(T, 'includeTo', function (entities, modelName, clause) {
+      expect(entities).to.be.eql([MODEL_DATA]);
+      expect(modelName).to.be.eql('model');
+      expect(clause).to.be.eql(FILTER.include);
+      Object.assign(entities[0], RETVAL_DATA);
+    });
+    const retval = await A.findById('model', 1, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.includeTo).to.be.called.once;
+  });
+});

+ 5 - 0
src/adapter/decorator/index.js

@@ -0,0 +1,5 @@
+export * from './inclusion-decorator.js';
+export * from './default-values-decorator.js';
+export * from './data-sanitizing-decorator.js';
+export * from './data-validation-decorator.js';
+export * from './fields-filtering-decorator.js';

+ 3 - 0
src/adapter/index.js

@@ -0,0 +1,3 @@
+export * from './adapter.js';
+export * from './adapter-loader.js';
+export * from './adapter-registry.js';

+ 33 - 0
src/definition/datasource/datasource-definition-validator.js

@@ -0,0 +1,33 @@
+import {Service} from '../../service/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+
+/**
+ * Datasource definition validator.
+ */
+export class DatasourceDefinitionValidator extends Service {
+  /**
+   * Validate.
+   *
+   * @param datasourceDef
+   */
+  validate(datasourceDef) {
+    if (!datasourceDef || typeof datasourceDef !== 'object')
+      throw new InvalidArgumentError(
+        'The datasource definition should be an Object, but %s given.',
+        datasourceDef,
+      );
+    if (!datasourceDef.name || typeof datasourceDef.name !== 'string')
+      throw new InvalidArgumentError(
+        'The datasource definition requires the option "name" ' +
+          'as a non-empty String, but %s given.',
+        datasourceDef.name,
+      );
+    if (!datasourceDef.adapter || typeof datasourceDef.adapter !== 'string')
+      throw new InvalidArgumentError(
+        'The datasource %s requires the option "adapter" ' +
+          'as a non-empty String, but %s given.',
+        datasourceDef.name,
+        datasourceDef.adapter,
+      );
+  }
+}

+ 64 - 0
src/definition/datasource/datasource-definition-validator.spec.js

@@ -0,0 +1,64 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {DatasourceDefinitionValidator} from './datasource-definition-validator.js';
+
+const S = new DatasourceDefinitionValidator();
+
+describe('DatasourceDefinitionValidator', function () {
+  describe('validate', function () {
+    it('requires a given definition to be an object', function () {
+      const validate = value => () => S.validate(value);
+      const error = value =>
+        format(
+          'The datasource definition should be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate({name: 'datasource', adapter: 'adapter'})();
+    });
+
+    it('requires the option "name" as a non-empty string', function () {
+      const validate = name => () => S.validate({name, adapter: 'adapter'});
+      const error = value =>
+        format(
+          'The datasource definition requires the option "name" ' +
+            'as a non-empty String, but %s given.',
+          value,
+        );
+      expect(validate('')).to.throw(error('""'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate('datasource')();
+    });
+
+    it('requires the option "adapter" to be a non-empty string', function () {
+      const validate = adapter => () =>
+        S.validate({name: 'datasource', adapter});
+      const error = value =>
+        format(
+          'The datasource "datasource" requires the option "adapter" ' +
+            'as a non-empty String, but %s given.',
+          value,
+        );
+      expect(validate('')).to.throw(error('""'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate('adapter')();
+    });
+  });
+});

+ 1 - 0
src/definition/datasource/index.js

@@ -0,0 +1 @@
+export * from './datasource-definition-validator.js';

+ 98 - 0
src/definition/definition-registry.js

@@ -0,0 +1,98 @@
+import {Service} from '../service/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {ModelDefinitionValidator} from './model/index.js';
+import {DatasourceDefinitionValidator} from '../definition/index.js';
+
+/**
+ * Definition registry.
+ */
+export class DefinitionRegistry extends Service {
+  /**
+   * Datasources.
+   *
+   * @type {{[name: string]: object}}
+   */
+  _datasources = {};
+
+  /**
+   * Models.
+   *
+   * @type {{[name: string]: object}}
+   */
+  _models = {};
+
+  /**
+   * Add datasource.
+   *
+   * @param datasourceDef
+   */
+  addDatasource(datasourceDef) {
+    this.get(DatasourceDefinitionValidator).validate(datasourceDef);
+    const name = datasourceDef.name;
+    if (name in this._datasources)
+      throw new InvalidArgumentError(
+        'The datasource %s is already defined.',
+        name,
+      );
+    this._datasources[name] = datasourceDef;
+  }
+
+  /**
+   * Has datasource.
+   *
+   * @param name
+   * @return {*}
+   */
+  hasDatasource(name) {
+    return Boolean(this._datasources[name]);
+  }
+
+  /**
+   * Get datasource.
+   *
+   * @param name
+   * @return {Object}
+   */
+  getDatasource(name) {
+    const datasourceDef = this._datasources[name];
+    if (!datasourceDef)
+      throw new InvalidArgumentError('The datasource %s is not defined.', name);
+    return datasourceDef;
+  }
+
+  /**
+   * Add model.
+   *
+   * @param modelDef
+   */
+  addModel(modelDef) {
+    this.get(ModelDefinitionValidator).validate(modelDef);
+    const name = modelDef.name;
+    if (name in this._models)
+      throw new InvalidArgumentError('The model %s is already defined.', name);
+    this._models[name] = modelDef;
+  }
+
+  /**
+   * Has model.
+   *
+   * @param name
+   * @return {*}
+   */
+  hasModel(name) {
+    return Boolean(this._models[name]);
+  }
+
+  /**
+   * Get model.
+   *
+   * @param name
+   * @return {Object}
+   */
+  getModel(name) {
+    const modelDef = this._models[name];
+    if (!modelDef)
+      throw new InvalidArgumentError('The model %s is not defined.', name);
+    return modelDef;
+  }
+}

+ 78 - 0
src/definition/definition-registry.spec.js

@@ -0,0 +1,78 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {ModelDefinitionValidator} from './model/index.js';
+import {DefinitionRegistry} from './definition-registry.js';
+import {DatasourceDefinitionValidator} from '../definition/index.js';
+
+const sandbox = chai.spy.sandbox();
+
+describe('DefinitionRegistry', function () {
+  let S;
+  beforeEach(function () {
+    S = new DefinitionRegistry();
+  });
+
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  it('sets a given datasource to the state', function () {
+    const datasource = {name: 'datasource', adapter: 'adapter'};
+    S.addDatasource(datasource);
+    const result = S.getDatasource('datasource');
+    expect(result).to.be.eql(datasource);
+  });
+
+  it('throws an error if a given datasource is already defined', function () {
+    const datasource1 = {name: 'datasource', adapter: 'adapter'};
+    const datasource2 = {name: 'datasource', adapter: 'adapter'};
+    S.addDatasource(datasource1);
+    const throwable = () => S.addDatasource(datasource2);
+    expect(throwable).to.throw(
+      'The datasource "datasource" is already defined.',
+    );
+  });
+
+  it('throws an error when getting a not defined datasource', function () {
+    const throwable = () => S.getDatasource('undefined');
+    expect(throwable).to.throw('The datasource "undefined" is not defined.');
+  });
+
+  it('uses DatasourceDefinitionValidator to validate a given datasource', function () {
+    const V = S.get(DatasourceDefinitionValidator);
+    sandbox.on(V, 'validate');
+    const datasource = {name: 'datasource', adapter: 'adapter'};
+    S.addDatasource(datasource);
+    expect(V.validate).to.have.been.called.once;
+    expect(V.validate).to.have.been.called.with.exactly(datasource);
+  });
+
+  it('sets a given model to the state', function () {
+    const model = {name: 'model'};
+    S.addModel(model);
+    const result = S.getModel('model');
+    expect(result).to.be.eql(model);
+  });
+
+  it('throws an error if a given model is already defined', function () {
+    const model1 = {name: 'model'};
+    const model2 = {name: 'model'};
+    S.addModel(model1);
+    const throwable = () => S.addModel(model2);
+    expect(throwable).to.throw('The model "model" is already defined.');
+  });
+
+  it('throws an error when getting a not defined model', function () {
+    const throwable = () => S.getModel('undefined');
+    expect(throwable).to.throw('The model "undefined" is not defined.');
+  });
+
+  it('uses ModelDefinitionValidator to validate a given model', function () {
+    const V = S.get(ModelDefinitionValidator);
+    sandbox.on(V, 'validate');
+    const model = {name: 'model'};
+    S.addModel(model);
+    expect(V.validate).to.have.been.called.once;
+    expect(V.validate).to.have.been.called.with.exactly(model);
+  });
+});

+ 3 - 0
src/definition/index.js

@@ -0,0 +1,3 @@
+export * from './model/index.js';
+export * from './datasource/index.js';
+export * from './definition-registry.js';

+ 6 - 0
src/definition/model/index.js

@@ -0,0 +1,6 @@
+export * from './relations/index.js';
+export * from './properties/index.js';
+export * from './model-data-validator.js';
+export * from './model-data-sanitizer.js';
+export * from './model-definition-utils.js';
+export * from './model-definition-validator.js';

+ 34 - 0
src/definition/model/model-data-sanitizer.js

@@ -0,0 +1,34 @@
+import {Service} from '../../service/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {ModelDefinitionUtils} from './model-definition-utils.js';
+
+/**
+ * Model data validator.
+ */
+export class ModelDataSanitizer extends Service {
+  /**
+   * Validate.
+   *
+   * @param {string} modelName
+   * @param {object} modelData
+   * @return {object}
+   */
+  sanitize(modelName, modelData) {
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'The first argument of ModelDataSanitizer.sanitize ' +
+          'must be a string, but %s given.',
+        modelName,
+      );
+    if (!modelData || typeof modelData !== 'object')
+      throw new InvalidArgumentError(
+        'The second argument of ModelDataSanitizer.sanitize ' +
+          'must be an Object, but %s given.',
+        modelData,
+      );
+    return this.get(ModelDefinitionUtils).excludeObjectKeysByRelationNames(
+      modelName,
+      modelData,
+    );
+  }
+}

+ 144 - 0
src/definition/model/model-data-validator.js

@@ -0,0 +1,144 @@
+import {Service} from '../../service/index.js';
+import {DataType} from './properties/index.js';
+import {getCtorName} from '../../utils/index.js';
+import {isPureObject} from '../../utils/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {ModelDefinitionUtils} from './model-definition-utils.js';
+
+/**
+ * Model data validator.
+ */
+export class ModelDataValidator extends Service {
+  /**
+   * Validate.
+   *
+   * @param modelName
+   * @param modelData
+   * @param isPartial
+   */
+  validate(modelName, modelData, isPartial = false) {
+    if (!isPureObject(modelData))
+      throw new InvalidArgumentError(
+        'The data of the model %s must be an Object, but %s given.',
+        modelName,
+        modelData,
+      );
+    const propDefs =
+      this.get(
+        ModelDefinitionUtils,
+      ).getPropertiesDefinitionInBaseModelHierarchy(modelName);
+    const propNames = Object.keys(isPartial ? modelData : propDefs);
+    propNames.forEach(propName => {
+      const propDef = propDefs[propName];
+      if (!propDef) return;
+      this.validatePropertyValue(
+        modelName,
+        propName,
+        propDef,
+        modelData[propName],
+      );
+    });
+  }
+
+  /**
+   * Validate property value.
+   *
+   * @param modelName
+   * @param propName
+   * @param propDef
+   * @param propValue
+   */
+  validatePropertyValue(modelName, propName, propDef, propValue) {
+    // undefined and null
+    if (propValue == null) {
+      const isRequired =
+        typeof propDef === 'string' ? false : Boolean(propDef.required);
+      if (!isRequired) return;
+      throw new InvalidArgumentError(
+        'The property %s of the model %s is required, but %s given.',
+        propName,
+        modelName,
+        propValue,
+      );
+    }
+    // DataType
+    this._validatePropertyValueType(modelName, propName, propDef, propValue);
+  }
+
+  /**
+   * Validate value type.
+   *
+   * @param modelName
+   * @param propName
+   * @param propDef
+   * @param propValue
+   * @param isArrayValue
+   */
+  _validatePropertyValueType(
+    modelName,
+    propName,
+    propDef,
+    propValue,
+    isArrayValue = false,
+  ) {
+    let expectingType;
+    if (isArrayValue) {
+      if (typeof propDef === 'object') {
+        expectingType = propDef.itemType ?? DataType.ANY;
+      } else {
+        expectingType = DataType.ANY;
+      }
+    } else {
+      expectingType = typeof propDef !== 'string' ? propDef.type : propDef;
+    }
+
+    const createError = expected => {
+      const pattern = isArrayValue
+        ? 'The array property %s of the model %s must have %s element, but %s given.'
+        : 'The property %s of the model %s must have %s, but %s given.';
+      const expectedStr = new String(expected);
+      const ctorName = getCtorName(propValue);
+      const givenStr = new String(ctorName ?? typeof propValue);
+      return new InvalidArgumentError(
+        pattern,
+        propName,
+        modelName,
+        expectedStr,
+        givenStr,
+      );
+    };
+    switch (expectingType) {
+      // STRING
+      case DataType.STRING:
+        if (typeof propValue !== 'string') throw createError('a String');
+        break;
+      // NUMBER
+      case DataType.NUMBER:
+        if (typeof propValue !== 'number') throw createError('a Number');
+        break;
+      // BOOLEAN
+      case DataType.BOOLEAN:
+        if (typeof propValue !== 'boolean') throw createError('a Boolean');
+        break;
+      // ARRAY
+      case DataType.ARRAY:
+        if (!Array.isArray(propValue)) throw createError('an Array');
+        propValue.forEach(value =>
+          this._validatePropertyValueType(
+            modelName,
+            propName,
+            propDef,
+            value,
+            true,
+          ),
+        );
+        break;
+      // OBJECT
+      case DataType.OBJECT:
+        if (!isPureObject(propValue)) throw createError('an Object');
+        if (typeof propDef === 'object' && propDef.model)
+          this.validate(propDef.model, propValue);
+        break;
+    }
+  }
+}

+ 1885 - 0
src/definition/model/model-data-validator.spec.js

@@ -0,0 +1,1885 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {Schema} from '../../schema.js';
+import {DataType} from './properties/index.js';
+import {ModelDataValidator} from './model-data-validator.js';
+
+describe('ModelDataValidator', function () {
+  describe('validate', function () {
+    it('does not throw an error if a model does not have a property of a given data', function () {
+      const schema = new Schema();
+      schema.defineModel({name: 'model'});
+      schema.get(ModelDataValidator).validate('model', {foo: 'bar'});
+    });
+
+    it('throws an error if a given data is not a pure object', function () {
+      const throwable = modelData => {
+        const schema = new Schema();
+        schema.defineModel({
+          name: 'model',
+          datasource: 'datasource',
+        });
+        return () =>
+          schema.get(ModelDataValidator).validate('model', modelData);
+      };
+      const error = given =>
+        format(
+          'The data of the model "model" must be an Object, but %s given.',
+          given,
+        );
+      expect(throwable('string')).to.throw(error('"string"'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+    });
+
+    it('uses a base model hierarchy to validate a given data', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: DataType.STRING,
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const throwable = () =>
+        schema.get(ModelDataValidator).validate('modelB', {foo: 10});
+      expect(throwable).to.throw(
+        'The property "foo" of the model "modelB" must ' +
+          'have a String, but Number given.',
+      );
+    });
+
+    it('throws an error if a given data does not have a required property', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            required: true,
+          },
+        },
+      });
+      const throwable = () =>
+        schema.get(ModelDataValidator).validate('model', {});
+      expect(throwable).to.throw(
+        'The property "foo" of the model "model" ' +
+          'is required, but undefined given.',
+      );
+    });
+
+    it('throws an error if a required property is undefined', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            required: true,
+          },
+        },
+      });
+      const throwable = () =>
+        schema.get(ModelDataValidator).validate('model', {foo: undefined});
+      expect(throwable).to.throw(
+        'The property "foo" of the model "model" is required, but undefined given.',
+      );
+    });
+
+    it('throws an error if a required property is null', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            required: true,
+          },
+        },
+      });
+      const throwable = () =>
+        schema.get(ModelDataValidator).validate('model', {foo: null});
+      expect(throwable).to.throw(
+        'The property "foo" of the model "model" is required, but null given.',
+      );
+    });
+
+    describe('an option "isPartial" is true', function () {
+      it('does not throw an error if a given data does not have a required property', function () {
+        const schema = new Schema();
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              required: true,
+            },
+          },
+        });
+        schema.get(ModelDataValidator).validate('model', {}, true);
+      });
+
+      it('throws an error if a required property is undefined', function () {
+        const schema = new Schema();
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              required: true,
+            },
+          },
+        });
+        const throwable = () =>
+          schema
+            .get(ModelDataValidator)
+            .validate('model', {foo: undefined}, true);
+        expect(throwable).to.throw(
+          'The property "foo" of the model "model" ' +
+            'is required, but undefined given.',
+        );
+      });
+
+      it('throws an error if a required property is null', function () {
+        const schema = new Schema();
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              required: true,
+            },
+          },
+        });
+        const throwable = () =>
+          schema.get(ModelDataValidator).validate('model', {foo: null}, true);
+        expect(throwable).to.throw(
+          'The property "foo" of the model "model" is required, but null given.',
+        );
+      });
+    });
+
+    describe('DataType.ANY', function () {
+      describe('ShortPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ANY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ANY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('does not throw an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ANY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: 'bar',
+          });
+        });
+
+        it('does not throw an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ANY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: 10,
+          });
+        });
+
+        it('does not throw an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ANY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: true,
+          });
+        });
+
+        it('does not throw an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ANY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: false,
+          });
+        });
+
+        it('does not throw an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ANY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: [],
+          });
+        });
+
+        it('does not throw an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ANY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: {},
+          });
+        });
+      });
+
+      describe('FullPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ANY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ANY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('does not throw an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ANY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: 'bar',
+          });
+        });
+
+        it('does not throw an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ANY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: 10,
+          });
+        });
+
+        it('does not throw an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ANY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: true,
+          });
+        });
+
+        it('does not throw an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ANY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: false,
+          });
+        });
+
+        it('does not throw an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ANY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: [],
+          });
+        });
+
+        it('does not throw an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ANY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: {},
+          });
+        });
+      });
+    });
+
+    describe('DataType.STRING', function () {
+      describe('ShortPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.STRING,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.STRING,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('does not throw an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.STRING,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: 'bar',
+          });
+        });
+
+        it('throws an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.STRING,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 10,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Number given.',
+          );
+        });
+
+        it('throws an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.STRING,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: true,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Boolean given.',
+          );
+        });
+
+        it('throws an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.STRING,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: false,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Boolean given.',
+          );
+        });
+
+        it('throws an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.STRING,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: [],
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Array given.',
+          );
+        });
+
+        it('throws an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.STRING,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: {},
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Object given.',
+          );
+        });
+      });
+
+      describe('FullPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.STRING,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.STRING,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('does not throw an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.STRING,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: 'bar',
+          });
+        });
+
+        it('throws an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.STRING,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 10,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Number given.',
+          );
+        });
+
+        it('throws an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.STRING,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: true,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Boolean given.',
+          );
+        });
+
+        it('throws an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.STRING,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: false,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Boolean given.',
+          );
+        });
+
+        it('throws an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.STRING,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: [],
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Array given.',
+          );
+        });
+
+        it('throws an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.STRING,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: {},
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a String, but Object given.',
+          );
+        });
+      });
+    });
+
+    describe('DataType.NUMBER', function () {
+      describe('ShortPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.NUMBER,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.NUMBER,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('throws an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.NUMBER,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 'bar',
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but String given.',
+          );
+        });
+
+        it('does not throw an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.NUMBER,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: 10,
+          });
+        });
+
+        it('throws an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.NUMBER,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: true,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but Boolean given.',
+          );
+        });
+
+        it('throws an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.NUMBER,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: false,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but Boolean given.',
+          );
+        });
+
+        it('throws an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.NUMBER,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: [],
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but Array given.',
+          );
+        });
+
+        it('throws an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.NUMBER,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: {},
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but Object given.',
+          );
+        });
+      });
+
+      describe('FullPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.NUMBER,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.NUMBER,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('throws an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.NUMBER,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 'bar',
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but String given.',
+          );
+        });
+
+        it('does not throw an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.NUMBER,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: 10,
+          });
+        });
+
+        it('throws an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.NUMBER,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: true,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but Boolean given.',
+          );
+        });
+
+        it('throws an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.NUMBER,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: false,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but Boolean given.',
+          );
+        });
+
+        it('throws an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.NUMBER,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: [],
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but Array given.',
+          );
+        });
+
+        it('throws an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.NUMBER,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: {},
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Number, but Object given.',
+          );
+        });
+      });
+    });
+
+    describe('DataType.BOOLEAN', function () {
+      describe('ShortPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.BOOLEAN,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.BOOLEAN,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('throws an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.BOOLEAN,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 'bar',
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Boolean, but String given.',
+          );
+        });
+
+        it('throws an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.BOOLEAN,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 10,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Boolean, but Number given.',
+          );
+        });
+
+        it('does not throw an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.BOOLEAN,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: true,
+          });
+        });
+
+        it('does not throw an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.BOOLEAN,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: false,
+          });
+        });
+
+        it('throws an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.BOOLEAN,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: [],
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Boolean, but Array given.',
+          );
+        });
+
+        it('throws an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.BOOLEAN,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: {},
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Boolean, but Object given.',
+          );
+        });
+      });
+
+      describe('FullPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.BOOLEAN,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.BOOLEAN,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('throws an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.BOOLEAN,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 'bar',
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Boolean, but String given.',
+          );
+        });
+
+        it('throws an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.BOOLEAN,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 10,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Boolean, but Number given.',
+          );
+        });
+
+        it('does not throw an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.BOOLEAN,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: true,
+          });
+        });
+
+        it('does not throw an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.BOOLEAN,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: false,
+          });
+        });
+
+        it('throws an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.BOOLEAN,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: [],
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Boolean, but Array given.',
+          );
+        });
+
+        it('throws an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.BOOLEAN,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: {},
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'a Boolean, but Object given.',
+          );
+        });
+      });
+    });
+
+    describe('DataType.ARRAY', function () {
+      describe('ShortPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ARRAY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ARRAY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('throws an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ARRAY,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 'bar',
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but String given.',
+          );
+        });
+
+        it('throws an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ARRAY,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 10,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but Number given.',
+          );
+        });
+
+        it('throws an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ARRAY,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: true,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but Boolean given.',
+          );
+        });
+
+        it('throws an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ARRAY,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: false,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but Boolean given.',
+          );
+        });
+
+        it('does not throw an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ARRAY,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: [],
+          });
+        });
+
+        it('throws an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.ARRAY,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: {},
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but Object given.',
+          );
+        });
+      });
+
+      describe('FullPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ARRAY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ARRAY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('throws an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ARRAY,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 'bar',
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but String given.',
+          );
+        });
+
+        it('throws an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ARRAY,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 10,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but Number given.',
+          );
+        });
+
+        it('throws an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ARRAY,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: true,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but Boolean given.',
+          );
+        });
+
+        it('throws an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ARRAY,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: false,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but Boolean given.',
+          );
+        });
+
+        it('does not throw an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ARRAY,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: [],
+          });
+        });
+
+        it('throws an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.ARRAY,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: {},
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Array, but Object given.',
+          );
+        });
+      });
+    });
+
+    describe('DataType.OBJECT', function () {
+      describe('ShortPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.OBJECT,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.OBJECT,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('throws an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.OBJECT,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 'bar',
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but String given.',
+          );
+        });
+
+        it('throws an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.OBJECT,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 10,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but Number given.',
+          );
+        });
+
+        it('throws an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.OBJECT,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: true,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but Boolean given.',
+          );
+        });
+
+        it('throws an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.OBJECT,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: false,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but Boolean given.',
+          );
+        });
+
+        it('throws an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.OBJECT,
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: [],
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but Array given.',
+          );
+        });
+
+        it('does not throw an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: DataType.OBJECT,
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: {},
+          });
+        });
+      });
+
+      describe('FullPropertyDefinition', function () {
+        it('does not throw an error if an undefined given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.OBJECT,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: undefined,
+          });
+        });
+
+        it('does not throw an error if a null given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.OBJECT,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: null,
+          });
+        });
+
+        it('throws an error if a string given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.OBJECT,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 'bar',
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but String given.',
+          );
+        });
+
+        it('throws an error if a number given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.OBJECT,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: 10,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but Number given.',
+          );
+        });
+
+        it('throws an error if true given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.OBJECT,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: true,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but Boolean given.',
+          );
+        });
+
+        it('throws an error if false given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.OBJECT,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: false,
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but Boolean given.',
+          );
+        });
+
+        it('throws an error if an array given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.OBJECT,
+              },
+            },
+          });
+          const throwable = () =>
+            S.get(ModelDataValidator).validate('model', {
+              foo: [],
+            });
+          expect(throwable).to.throw(
+            'The property "foo" of the model "model" must have ' +
+              'an Object, but Array given.',
+          );
+        });
+
+        it('does not throw an error if an object given', function () {
+          const S = new Schema();
+          S.defineModel({
+            name: 'model',
+            datasource: 'datasource',
+            properties: {
+              foo: {
+                type: DataType.OBJECT,
+              },
+            },
+          });
+          S.get(ModelDataValidator).validate('model', {
+            foo: {},
+          });
+        });
+      });
+    });
+  });
+});

+ 360 - 0
src/definition/model/model-definition-utils.js

@@ -0,0 +1,360 @@
+import {Service} from '../../service/index.js';
+import {DataType} from './properties/index.js';
+import {cloneDeep} from '../../utils/index.js';
+import {excludeObjectKeys} from '../../utils/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {DefinitionRegistry} from '../definition-registry.js';
+
+/**
+ * Default primary key property name.
+ *
+ * @type {string}
+ */
+export const DEFAULT_PRIMARY_KEY_PROPERTY_NAME = 'id';
+
+/**
+ * Model definition utils.
+ */
+export class ModelDefinitionUtils extends Service {
+  /**
+   * Get primary key as property name.
+   *
+   * @param modelName
+   */
+  getPrimaryKeyAsPropertyName(modelName) {
+    const propDefs =
+      this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
+    const propNames = Object.keys(propDefs).filter(propName => {
+      const propDef = propDefs[propName];
+      return propDef && typeof propDef === 'object' && propDef.primaryKey;
+    });
+    if (propNames.length < 1) {
+      const isDefaultPrimaryKeyAlreadyInUse = Object.keys(propDefs).includes(
+        DEFAULT_PRIMARY_KEY_PROPERTY_NAME,
+      );
+      if (isDefaultPrimaryKeyAlreadyInUse)
+        throw new InvalidArgumentError(
+          'The property name %s of the model %s is defined as a regular property. ' +
+            'In this case, a primary key should be defined explicitly. ' +
+            'Do use the option "primaryKey" to specify the primary key.',
+          DEFAULT_PRIMARY_KEY_PROPERTY_NAME,
+          modelName,
+        );
+      return DEFAULT_PRIMARY_KEY_PROPERTY_NAME;
+    }
+    return propNames[0];
+  }
+
+  /**
+   * Get primary key as column name.
+   *
+   * @param modelName
+   */
+  getPrimaryKeyAsColumnName(modelName) {
+    const pkPropName = this.getPrimaryKeyAsPropertyName(modelName);
+    let pkColName;
+    try {
+      pkColName = this.getColumnNameByPropertyName(modelName, pkPropName);
+    } catch (error) {
+      if (!(error instanceof InvalidArgumentError)) throw error;
+    }
+    if (pkColName === undefined) return pkPropName;
+    return pkColName;
+  }
+
+  /**
+   * Get table name by model name.
+   *
+   * @param modelName
+   */
+  getTableNameByModelName(modelName) {
+    const modelDef = this.get(DefinitionRegistry).getModel(modelName);
+    return modelDef.tableName ?? modelName;
+  }
+
+  /**
+   * Get column name by property name.
+   *
+   * @param modelName
+   * @param propertyName
+   */
+  getColumnNameByPropertyName(modelName, propertyName) {
+    const propDefs =
+      this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
+    const propDef = propDefs[propertyName];
+    if (!propDef)
+      throw new InvalidArgumentError(
+        'The model %s does not have the property %s.',
+        modelName,
+        propertyName,
+      );
+    if (propDef && typeof propDef === 'object')
+      return propDef.columnName ?? propertyName;
+    return propertyName;
+  }
+
+  /**
+   * Get default property value.
+   *
+   * @param modelName
+   * @param propertyName
+   */
+  getDefaultPropertyValue(modelName, propertyName) {
+    const propDefs =
+      this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
+    const propDef = propDefs[propertyName];
+    if (!propDef)
+      throw new InvalidArgumentError(
+        'The model %s does not have the property %s.',
+        modelName,
+        propertyName,
+      );
+    if (propDef && typeof propDef === 'object')
+      return propDef.default instanceof Function
+        ? propDef.default()
+        : propDef.default;
+  }
+
+  /**
+   * Set default values for empty properties.
+   *
+   * @param modelName
+   * @param modelData
+   * @param onlyProvidedProperties
+   */
+  setDefaultValuesToEmptyProperties(
+    modelName,
+    modelData,
+    onlyProvidedProperties = false,
+  ) {
+    const propDefs =
+      this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
+    const propNames = onlyProvidedProperties
+      ? Object.keys(modelData)
+      : Object.keys(propDefs);
+    const extendedData = cloneDeep(modelData);
+    propNames.forEach(propName => {
+      const value = extendedData[propName];
+      if (value != null) return;
+      const propDef = propDefs[propName];
+      if (
+        propDef &&
+        typeof propDef === 'object' &&
+        propDef.default !== undefined
+      ) {
+        extendedData[propName] = this.getDefaultPropertyValue(
+          modelName,
+          propName,
+        );
+      }
+    });
+    return extendedData;
+  }
+
+  /**
+   * Convert property names to column names.
+   *
+   * @param modelName
+   * @param modelData
+   */
+  convertPropertyNamesToColumnNames(modelName, modelData) {
+    const propDefs =
+      this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
+    const propNames = Object.keys(propDefs);
+    const convertedData = cloneDeep(modelData);
+    propNames.forEach(propName => {
+      if (!(propName in convertedData)) return;
+      const colName = this.getColumnNameByPropertyName(modelName, propName);
+      if (propName === colName) return;
+      const propValue = convertedData[propName];
+      delete convertedData[propName];
+      convertedData[colName] = propValue;
+    });
+    return convertedData;
+  }
+
+  /**
+   * Convert column names to property names.
+   *
+   * @param modelName
+   * @param tableData
+   */
+  convertColumnNamesToPropertyNames(modelName, tableData) {
+    const propDefs =
+      this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
+    const propNames = Object.keys(propDefs);
+    const convertedData = cloneDeep(tableData);
+    propNames.forEach(propName => {
+      const colName = this.getColumnNameByPropertyName(modelName, propName);
+      if (!(colName in convertedData) || colName === propName) return;
+      const colValue = convertedData[colName];
+      delete convertedData[colName];
+      convertedData[propName] = colValue;
+    });
+    return convertedData;
+  }
+
+  /**
+   * Get data type by property name.
+   *
+   * @param modelName
+   * @param propertyName
+   */
+  getDataTypeByPropertyName(modelName, propertyName) {
+    const propDefs =
+      this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
+    const propDef = propDefs[propertyName];
+    if (!propDef) {
+      const pkPropName = this.getPrimaryKeyAsPropertyName(modelName);
+      if (pkPropName === propertyName) return DataType.ANY;
+      throw new InvalidArgumentError(
+        'The model %s does not have the property %s.',
+        modelName,
+        propertyName,
+      );
+    }
+    if (typeof propDef === 'string') return propDef;
+    return propDef.type;
+  }
+
+  /**
+   * Get own properties definition of primary keys.
+   *
+   * @param modelName
+   * @return {Record<string, {}>}
+   */
+  getOwnPropertiesDefinitionOfPrimaryKeys(modelName) {
+    const modelDef = this.get(DefinitionRegistry).getModel(modelName);
+    const propDefs = modelDef.properties ?? {};
+    const pkPropNames = Object.keys(propDefs).filter(propName => {
+      const propDef = propDefs[propName];
+      return typeof propDef === 'object' && propDef.primaryKey;
+    });
+    return pkPropNames.reduce((a, k) => ({...a, [k]: propDefs[k]}), {});
+  }
+
+  /**
+   * Get own properties definition without primary keys.
+   *
+   * @param modelName
+   * @return {Record<string, {}>}
+   */
+  getOwnPropertiesDefinitionWithoutPrimaryKeys(modelName) {
+    const modelDef = this.get(DefinitionRegistry).getModel(modelName);
+    const propDefs = modelDef.properties ?? {};
+    return Object.keys(propDefs).reduce((result, propName) => {
+      const propDef = propDefs[propName];
+      if (typeof propDef === 'object' && propDef.primaryKey) return result;
+      return {...result, [propName]: propDef};
+    }, {});
+  }
+
+  /**
+   * Get properties definition in base model hierarchy.
+   *
+   * @param modelName
+   * @return {Record<string, {}>}
+   */
+  getPropertiesDefinitionInBaseModelHierarchy(modelName) {
+    let result = {};
+    let pkPropDefs = {};
+    const recursion = (currModelName, prevModelName = undefined) => {
+      if (currModelName === prevModelName)
+        throw new InvalidArgumentError(
+          'The model %s has a circular inheritance.',
+          currModelName,
+        );
+      if (Object.keys(pkPropDefs).length === 0) {
+        pkPropDefs =
+          this.getOwnPropertiesDefinitionOfPrimaryKeys(currModelName);
+        result = {...result, ...pkPropDefs};
+      }
+      const regularPropDefs =
+        this.getOwnPropertiesDefinitionWithoutPrimaryKeys(currModelName);
+      result = {...regularPropDefs, ...result};
+      const modelDef = this.get(DefinitionRegistry).getModel(currModelName);
+      if (modelDef.base) recursion(modelDef.base, currModelName);
+    };
+    recursion(modelName);
+    return result;
+  }
+
+  /**
+   * Get own relations definition.
+   *
+   * @param modelName
+   * @return {Record<string, {}>}
+   */
+  getOwnRelationsDefinition(modelName) {
+    const modelDef = this.get(DefinitionRegistry).getModel(modelName);
+    return modelDef.relations ?? {};
+  }
+
+  /**
+   * Get relations definition in base model hierarchy.
+   *
+   * @param modelName
+   * @return {Record<string, {}>}
+   */
+  getRelationsDefinitionInBaseModelHierarchy(modelName) {
+    let result = {};
+    const recursion = (currModelName, prevModelName = undefined) => {
+      if (currModelName === prevModelName)
+        throw new InvalidArgumentError(
+          'The model %s has a circular inheritance.',
+          currModelName,
+        );
+      const modelDef = this.get(DefinitionRegistry).getModel(currModelName);
+      const ownRelDefs = modelDef.relations ?? {};
+      result = {...ownRelDefs, ...result};
+      if (modelDef.base) recursion(modelDef.base, currModelName);
+    };
+    recursion(modelName);
+    return result;
+  }
+
+  /**
+   * Get relation definition by name.
+   *
+   * @param modelName
+   * @param relationName
+   * @return {Record<string, unknown>}
+   */
+  getRelationDefinitionByName(modelName, relationName) {
+    const relDefs = this.getRelationsDefinitionInBaseModelHierarchy(modelName);
+    const relNames = Object.keys(relDefs);
+    let foundDef;
+    for (const relName of relNames) {
+      if (relName === relationName) {
+        foundDef = relDefs[relName];
+        break;
+      }
+    }
+    if (!foundDef)
+      throw new InvalidArgumentError(
+        'The model %s does not have relation name %s.',
+        modelName,
+        relationName,
+      );
+    return foundDef;
+  }
+
+  /**
+   * Exclude object keys by relation names.
+   *
+   * @param {string} modelName
+   * @param {object} modelData
+   * @return {object}
+   */
+  excludeObjectKeysByRelationNames(modelName, modelData) {
+    if (!modelData || typeof modelData !== 'object' || Array.isArray(modelData))
+      throw new InvalidArgumentError(
+        'The second argument of ModelDefinitionUtils.excludeObjectKeysByRelationNames ' +
+          'must be an Object, but %s given.',
+        modelData,
+      );
+    const relDefs = this.getRelationsDefinitionInBaseModelHierarchy(modelName);
+    const relNames = Object.keys(relDefs);
+    return excludeObjectKeys(modelData, relNames);
+  }
+}

+ 1474 - 0
src/definition/model/model-definition-utils.spec.js

@@ -0,0 +1,1474 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {format} from 'util';
+import {Schema} from '../../schema.js';
+import {DataType} from './properties/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {ModelDefinitionUtils} from './model-definition-utils.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from './model-definition-utils.js';
+import {RelationType} from './relations/index.js';
+
+const sandbox = chai.spy.sandbox();
+
+describe('ModelDefinitionUtils', function () {
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  describe('getPrimaryKeyAsPropertyName', function () {
+    it('returns a default property name if no primary key defined', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsPropertyName('model');
+      expect(result).to.be.eq(DEF_PK);
+    });
+
+    it('throws an error if a property name of a default primary key already in use as a regular property', function () {
+      const schema = new Schema();
+      const S = schema.get(ModelDefinitionUtils);
+      sandbox.on(
+        S,
+        'getPropertiesDefinitionInBaseModelHierarchy',
+        function (modelName) {
+          expect(modelName).to.be.eq('model');
+          return {[DEF_PK]: DataType.NUMBER};
+        },
+      );
+      const throwable = () => S.getPrimaryKeyAsPropertyName('model');
+      expect(throwable).to.throw(
+        format(
+          'The property name "%s" of the model "model" is defined as a regular property. ' +
+            'In this case, a primary key should be defined explicitly. ' +
+            'Do use the option "primaryKey" to specify the primary key.',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('returns a property name if a primary key has a custom name and a default primary key is used as a regular property', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          [DEF_PK]: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsPropertyName('model');
+      expect(result).to.be.eql('myId');
+    });
+
+    it('returns a property name of a primary key', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          bar: {
+            type: DataType.NUMBER,
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsPropertyName('model');
+      expect(result).to.be.eq('foo');
+    });
+
+    it('uses a base model hierarchy to get a property name of a primary key', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsPropertyName('modelB');
+      expect(result).to.be.eq('foo');
+    });
+  });
+
+  describe('getPrimaryKeyAsColumnName', function () {
+    it('returns a property name of a primary key if a column name is not specified', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          bar: {
+            type: DataType.NUMBER,
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsColumnName('model');
+      expect(result).to.be.eq('foo');
+    });
+
+    it('returns a column name of a primary key if specified', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            columnName: 'fooColumn',
+          },
+          bar: {
+            type: DataType.NUMBER,
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsColumnName('model');
+      expect(result).to.be.eq('fooColumn');
+    });
+
+    it('returns a default property name if a primary key is not defined', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsColumnName('model');
+      expect(result).to.be.eq(DEF_PK);
+    });
+
+    it('throws an error if a property name of a default primary key already in use as a regular property', function () {
+      const schema = new Schema();
+      const S = schema.get(ModelDefinitionUtils);
+      sandbox.on(
+        S,
+        'getPropertiesDefinitionInBaseModelHierarchy',
+        function (modelName) {
+          expect(modelName).to.be.eq('model');
+          return {[DEF_PK]: DataType.NUMBER};
+        },
+      );
+      const throwable = () => S.getPrimaryKeyAsColumnName('model');
+      expect(throwable).to.throw(
+        format(
+          'The property name "%s" of the model "model" is defined as a regular property. ' +
+            'In this case, a primary key should be defined explicitly. ' +
+            'Do use the option "primaryKey" to specify the primary key.',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('returns a property name of a custom primary key when a default primary key is used as a regular property', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          [DEF_PK]: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsColumnName('model');
+      expect(result).to.be.eql('myId');
+    });
+
+    it('uses a base model hierarchy to get a column name of a primary key', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPrimaryKeyAsPropertyName('modelB');
+      expect(result).to.be.eq('foo');
+    });
+  });
+
+  describe('getTableNameByModelName', function () {
+    it('returns a model name if no table name specified', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getTableNameByModelName('model');
+      expect(result).to.be.eq('model');
+    });
+
+    it('returns a table name from a model definition', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        tableName: 'table',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getTableNameByModelName('model');
+      expect(result).to.be.eq('table');
+    });
+  });
+
+  describe('getColumnNameByPropertyName', function () {
+    it('returns a property name if a column name is not defined', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: DataType.STRING,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getColumnNameByPropertyName('model', 'foo');
+      expect(result).to.be.eq('foo');
+    });
+
+    it('returns a specified column name', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'bar',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getColumnNameByPropertyName('model', 'foo');
+      expect(result).to.be.eq('bar');
+    });
+
+    it('throws an error if a given property name does not exist', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const throwable = () =>
+        schema
+          .get(ModelDefinitionUtils)
+          .getColumnNameByPropertyName('model', 'foo');
+      expect(throwable).to.throw(InvalidArgumentError);
+    });
+
+    it('uses a base model hierarchy to get a specified column name', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooColumn',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getColumnNameByPropertyName('modelB', 'foo');
+      expect(result).to.be.eq('fooColumn');
+    });
+  });
+
+  describe('getDefaultPropertyValue', function () {
+    it('returns undefined if no default value specified', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: DataType.STRING,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getDefaultPropertyValue('model', 'foo');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns a default value from a property definition', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'default',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getDefaultPropertyValue('model', 'foo');
+      expect(result).to.be.eq('default');
+    });
+
+    it('returns a value from a factory function', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: () => 'default',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getDefaultPropertyValue('model', 'foo');
+      expect(result).to.be.eq('default');
+    });
+
+    it('throws an error if a given property name does not exist', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const throwable = () =>
+        schema
+          .get(ModelDefinitionUtils)
+          .getDefaultPropertyValue('model', 'foo');
+      expect(throwable).to.throw(InvalidArgumentError);
+    });
+
+    it('uses a base model hierarchy to get a specified default value', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'default',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getDefaultPropertyValue('modelB', 'foo');
+      expect(result).to.be.eq('default');
+    });
+  });
+
+  describe('setDefaultValuesToEmptyProperties', function () {
+    it('does nothing if no property definitions', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .setDefaultValuesToEmptyProperties('model', {foo: 'string'});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('does nothing if no "default" option in property definition', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          baz: DataType.STRING,
+          qux: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .setDefaultValuesToEmptyProperties('model', {foo: 'string'});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('sets a default value if a property does not exist', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .setDefaultValuesToEmptyProperties('model', {});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('sets a default value if a property is undefined', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .setDefaultValuesToEmptyProperties('model', {foo: undefined});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('sets a default value if a property is null', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .setDefaultValuesToEmptyProperties('model', {foo: null});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('sets a value from a factory function', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: () => 'string',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: () => 10,
+          },
+          baz: {
+            type: DataType.STRING,
+            default: () => null,
+          },
+          qux: {
+            type: DataType.STRING,
+            default: () => undefined,
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .setDefaultValuesToEmptyProperties('model', {});
+      expect(result).to.be.eql({
+        foo: 'string',
+        bar: 10,
+        baz: null,
+        qux: undefined,
+      });
+    });
+
+    it('uses a base model hierarchy to set a default values', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 10,
+          },
+          baz: {
+            type: DataType.STRING,
+            default: null,
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .setDefaultValuesToEmptyProperties('modelB', {});
+      expect(result).to.be.eql({
+        foo: 'string',
+        bar: 10,
+        baz: null,
+      });
+    });
+
+    describe('an option "onlyProvidedProperties" is true', function () {
+      it('does not set a default value if a property does not exist', function () {
+        const schema = new Schema();
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              default: 'string',
+            },
+          },
+        });
+        const result = schema
+          .get(ModelDefinitionUtils)
+          .setDefaultValuesToEmptyProperties('model', {}, true);
+        expect(result).to.be.eql({});
+      });
+
+      it('sets a default value if a property is undefined', function () {
+        const schema = new Schema();
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              default: 'string',
+            },
+          },
+        });
+        const result = schema
+          .get(ModelDefinitionUtils)
+          .setDefaultValuesToEmptyProperties('model', {foo: undefined}, true);
+        expect(result).to.be.eql({foo: 'string'});
+      });
+
+      it('sets a default value if a property is null', function () {
+        const schema = new Schema();
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              default: 'string',
+            },
+          },
+        });
+        const result = schema
+          .get(ModelDefinitionUtils)
+          .setDefaultValuesToEmptyProperties('model', {foo: null}, true);
+        expect(result).to.be.eql({foo: 'string'});
+      });
+    });
+  });
+
+  describe('convertPropertyNamesToColumnNames', function () {
+    it('does nothing if no property definitions', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .convertPropertyNamesToColumnNames('model', {foo: 'string'});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('does nothing if no column name specified', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .convertPropertyNamesToColumnNames('model', {foo: 'string'});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('replaces property names by column names', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooColumn',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barColumn',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .convertPropertyNamesToColumnNames('model', {foo: 'string'});
+      expect(result).to.be.eql({fooColumn: 'string'});
+    });
+
+    it('uses a base model hierarchy to replace property names by column names', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooColumn',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barColumn',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .convertPropertyNamesToColumnNames('modelB', {foo: 'string'});
+      expect(result).to.be.eql({fooColumn: 'string'});
+    });
+  });
+
+  describe('convertColumnNamesToPropertyNames', function () {
+    it('does nothing if no property definitions', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .convertColumnNamesToPropertyNames('model', {foo: 'string'});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('does nothing if no column name specified', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .convertColumnNamesToPropertyNames('model', {foo: 'string'});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('replaces column names by property names', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooColumn',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barColumn',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .convertColumnNamesToPropertyNames('model', {fooColumn: 'string'});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+
+    it('uses a base model hierarchy to replace column names by property names', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooColumn',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barColumn',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .convertColumnNamesToPropertyNames('modelA', {fooColumn: 'string'});
+      expect(result).to.be.eql({foo: 'string'});
+    });
+  });
+
+  describe('getDataTypeByPropertyName', function () {
+    it('returns a property type of a short property definition', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: DataType.STRING,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getDataTypeByPropertyName('model', 'foo');
+      expect(result).to.be.eq(DataType.STRING);
+    });
+
+    it('returns a property type of a full property definition', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getDataTypeByPropertyName('model', 'foo');
+      expect(result).to.be.eq(DataType.STRING);
+    });
+
+    it('throws an error if a property name does not exist', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const throwable = () =>
+        schema
+          .get(ModelDefinitionUtils)
+          .getDataTypeByPropertyName('model', 'property');
+      expect(throwable).to.throw(InvalidArgumentError);
+    });
+
+    it('uses a base model hierarchy to get a type from a short property definition', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: DataType.STRING,
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getDataTypeByPropertyName('modelB', 'foo');
+      expect(result).to.be.eq(DataType.STRING);
+    });
+
+    it('uses a base model hierarchy to get a type from a full property definition', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getDataTypeByPropertyName('modelB', 'foo');
+      expect(result).to.be.eq(DataType.STRING);
+    });
+  });
+
+  describe('getOwnPropertiesDefinitionWithoutPrimaryKeys', function () {
+    it('returns an empty object if a model does not have properties', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnPropertiesDefinitionWithoutPrimaryKeys('model');
+      expect(result).to.be.eql({});
+    });
+
+    it('returns a properties definition without primary keys', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnPropertiesDefinitionWithoutPrimaryKeys('model');
+      expect(result).to.be.eql({
+        foo: DataType.STRING,
+        bar: DataType.STRING,
+      });
+    });
+
+    it('returns its own properties definition even it has a base model properties', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          foo: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnPropertiesDefinitionWithoutPrimaryKeys('modelB');
+      expect(result).to.be.eql({
+        foo: DataType.NUMBER,
+      });
+    });
+  });
+
+  describe('getOwnPropertiesDefinitionOfPrimaryKeys', function () {
+    it('returns an empty object if a model does not have properties', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnPropertiesDefinitionOfPrimaryKeys('model');
+      expect(result).to.be.eql({});
+    });
+
+    it('returns a properties definition of primary keys', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnPropertiesDefinitionOfPrimaryKeys('model');
+      expect(result).to.be.eql({
+        id: {
+          type: DataType.STRING,
+          primaryKey: true,
+        },
+      });
+    });
+
+    it('returns its own properties definition even it has a base model properties', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          foo: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnPropertiesDefinitionOfPrimaryKeys('modelB');
+      expect(result).to.be.eql({
+        id: {
+          type: DataType.NUMBER,
+          primaryKey: true,
+        },
+      });
+    });
+  });
+
+  describe('getPropertiesDefinitionInBaseModelHierarchy', function () {
+    it('returns an empty object if a model does not have properties', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPropertiesDefinitionInBaseModelHierarchy('model');
+      expect(result).to.be.eql({});
+    });
+
+    it('returns a properties definition of a model', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        properties: {
+          id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPropertiesDefinitionInBaseModelHierarchy('model');
+      expect(result).to.be.eql({
+        id: {
+          type: DataType.STRING,
+          primaryKey: true,
+        },
+        foo: DataType.STRING,
+        bar: DataType.NUMBER,
+      });
+    });
+
+    it('returns a properties definition of an extended model', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          foo: DataType.STRING,
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        properties: {
+          bar: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPropertiesDefinitionInBaseModelHierarchy('modelB');
+      expect(result).to.be.eql({
+        foo: DataType.STRING,
+        bar: DataType.NUMBER,
+      });
+    });
+
+    it('uses child properties in priority over a base model properties', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          id: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          foo: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPropertiesDefinitionInBaseModelHierarchy('modelB');
+      expect(result).to.be.eql({
+        id: {
+          type: DataType.NUMBER,
+          primaryKey: true,
+        },
+        foo: DataType.NUMBER,
+        bar: DataType.STRING,
+      });
+    });
+
+    it('uses primary keys from a model closest to child model', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        properties: {
+          id1: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+          foo: DataType.STRING,
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        properties: {
+          id2: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+          bar: DataType.NUMBER,
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getPropertiesDefinitionInBaseModelHierarchy('modelB');
+      expect(result).to.be.eql({
+        id2: {
+          type: DataType.STRING,
+          primaryKey: true,
+        },
+        foo: DataType.STRING,
+        bar: DataType.NUMBER,
+      });
+    });
+
+    it('throws an error for a circular reference', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        base: 'model',
+      });
+      const throwable = () =>
+        schema
+          .get(ModelDefinitionUtils)
+          .getPropertiesDefinitionInBaseModelHierarchy('model');
+      expect(throwable).to.throw(
+        'The model "model" has a circular inheritance.',
+      );
+    });
+  });
+
+  describe('getOwnRelationsDefinition', function () {
+    it('returns an empty object if a model does not have relations', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnRelationsDefinition('model');
+      expect(result).to.be.eql({});
+    });
+
+    it('returns a relations definition by a given model', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        relations: {
+          foo: {
+            type: RelationType.BELONGS_TO,
+            model: 'model',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnRelationsDefinition('model');
+      expect(result).to.be.eql({
+        foo: {
+          type: RelationType.BELONGS_TO,
+          model: 'model',
+        },
+      });
+    });
+
+    it('returns its own relations definition even it has a base model relations', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        relations: {
+          foo: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        relations: {
+          bar: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelB',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getOwnRelationsDefinition('modelB');
+      expect(result).to.be.eql({
+        bar: {
+          type: RelationType.BELONGS_TO,
+          model: 'modelB',
+        },
+      });
+    });
+  });
+
+  describe('getRelationsDefinitionInBaseModelHierarchy', function () {
+    it('returns an empty object if a model does not have relations', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getRelationsDefinitionInBaseModelHierarchy('model');
+      expect(result).to.be.eql({});
+    });
+
+    it('returns a relations definition of a model', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        relations: {
+          foo: {
+            type: RelationType.BELONGS_TO,
+            model: 'model',
+          },
+          bar: {
+            type: RelationType.BELONGS_TO,
+            model: 'model',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getRelationsDefinitionInBaseModelHierarchy('model');
+      expect(result).to.be.eql({
+        foo: {
+          type: RelationType.BELONGS_TO,
+          model: 'model',
+        },
+        bar: {
+          type: RelationType.BELONGS_TO,
+          model: 'model',
+        },
+      });
+    });
+
+    it('returns a relations definition of an extended model', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        relations: {
+          foo: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        relations: {
+          bar: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelB',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getRelationsDefinitionInBaseModelHierarchy('modelB');
+      expect(result).to.be.eql({
+        foo: {
+          type: RelationType.BELONGS_TO,
+          model: 'modelA',
+        },
+        bar: {
+          type: RelationType.BELONGS_TO,
+          model: 'modelB',
+        },
+      });
+    });
+
+    it('uses child relations in priority over a base model relations', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        relations: {
+          foo: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+          bar: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        relations: {
+          foo: {
+            type: RelationType.REFERENCES_MANY,
+            model: 'modelB',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getRelationsDefinitionInBaseModelHierarchy('modelB');
+      expect(result).to.be.eql({
+        foo: {
+          type: RelationType.REFERENCES_MANY,
+          model: 'modelB',
+        },
+        bar: {
+          type: RelationType.BELONGS_TO,
+          model: 'modelA',
+        },
+      });
+    });
+
+    it('throws an error for a circular reference', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        base: 'model',
+      });
+      const throwable = () =>
+        schema
+          .get(ModelDefinitionUtils)
+          .getRelationsDefinitionInBaseModelHierarchy('model');
+      expect(throwable).to.throw(
+        'The model "model" has a circular inheritance.',
+      );
+    });
+  });
+
+  describe('getRelationDefinitionByName', function () {
+    it('throws an error if a given model is not found', function () {
+      const schema = new Schema();
+      const throwable = () =>
+        schema
+          .get(ModelDefinitionUtils)
+          .getRelationDefinitionByName('model', 'myRelation');
+      expect(throwable).to.throw('The model "model" is not defined.');
+    });
+
+    it('throws an error if a given relation is not found', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+      });
+      const throwable = () =>
+        schema
+          .get(ModelDefinitionUtils)
+          .getRelationDefinitionByName('model', 'myRelation');
+      expect(throwable).to.throw(
+        'The model "model" does not have relation name "myRelation".',
+      );
+    });
+
+    it('returns a relation definition by a given name', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        relations: {
+          myRelation: {
+            type: RelationType.BELONGS_TO,
+            model: 'model',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getRelationDefinitionByName('model', 'myRelation');
+      expect(result).to.be.eql({
+        type: RelationType.BELONGS_TO,
+        model: 'model',
+      });
+    });
+
+    it('uses a child relations in priority over a base model relations', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        relations: {
+          myRelation: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+        relations: {
+          myRelation: {
+            type: RelationType.REFERENCES_MANY,
+            model: 'modelA',
+          },
+        },
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getRelationDefinitionByName('modelB', 'myRelation');
+      expect(result).to.be.eql({
+        type: RelationType.REFERENCES_MANY,
+        model: 'modelA',
+      });
+    });
+
+    it('returns a base model relation if a given relation name is not found in a child model', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'modelA',
+        relations: {
+          myRelation: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+        },
+      });
+      schema.defineModel({
+        name: 'modelB',
+        base: 'modelA',
+      });
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .getRelationDefinitionByName('modelB', 'myRelation');
+      expect(result).to.be.eql({
+        type: RelationType.BELONGS_TO,
+        model: 'modelA',
+      });
+    });
+  });
+
+  describe('excludeObjectKeysByRelationNames', function () {
+    it('excludes object keys by relation names', function () {
+      const schema = new Schema();
+      schema.defineModel({
+        name: 'model',
+        relations: {
+          baz: {
+            type: RelationType.BELONGS_TO,
+            model: 'model',
+          },
+          qux: {
+            type: RelationType.BELONGS_TO,
+            model: 'model',
+          },
+        },
+      });
+      const input = {
+        foo: 'fooVal',
+        bar: {val: 'barVal'},
+        baz: 'bazVal',
+        qux: {val: 'quxVal'},
+      };
+      const result = schema
+        .get(ModelDefinitionUtils)
+        .excludeObjectKeysByRelationNames('model', input);
+      expect(result).to.be.eql({foo: 'fooVal', bar: {val: 'barVal'}});
+      expect(result).to.be.not.eq(input);
+    });
+
+    it('requires a given object as an object', function () {
+      const schema = new Schema();
+      schema.defineModel({name: 'model'});
+      const throwable = v => () =>
+        schema
+          .get(ModelDefinitionUtils)
+          .excludeObjectKeysByRelationNames('model', v);
+      const error = v =>
+        format(
+          'The second argument of ModelDefinitionUtils.excludeObjectKeysByRelationNames ' +
+            'must be an Object, but %s given.',
+          v,
+        );
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      expect(throwable(null)).to.throw(error('null'));
+      throwable({})();
+      throwable({foo: 'bar'})();
+    });
+  });
+});

+ 83 - 0
src/definition/model/model-definition-validator.js

@@ -0,0 +1,83 @@
+import {Service} from '../../service/index.js';
+import {InvalidArgumentError} from '../../errors/index.js';
+import {RelationsDefinitionValidator} from './relations/index.js';
+import {PropertiesDefinitionValidator} from './properties/index.js';
+
+/**
+ * Model definition validator.
+ */
+export class ModelDefinitionValidator extends Service {
+  /**
+   * Validate.
+   *
+   * @param modelDef
+   */
+  validate(modelDef) {
+    if (!modelDef || typeof modelDef !== 'object' || Array.isArray(modelDef))
+      throw new InvalidArgumentError(
+        'The model definition should be an Object, but %s given.',
+        modelDef,
+      );
+    if (!modelDef.name || typeof modelDef.name !== 'string')
+      throw new InvalidArgumentError(
+        'The model definition requires the option "name" ' +
+          'as a non-empty String, but %s given.',
+        modelDef.name,
+      );
+    if (modelDef.datasource && typeof modelDef.datasource !== 'string')
+      throw new InvalidArgumentError(
+        'The provided option "datasource" of the model %s ' +
+          'should be a String, but %s given.',
+        modelDef.name,
+        modelDef.datasource,
+      );
+    if (modelDef.base && typeof modelDef.base !== 'string')
+      throw new InvalidArgumentError(
+        'The provided option "base" of the model %s ' +
+          'should be a String, but %s given.',
+        modelDef.name,
+        modelDef.base,
+      );
+    if (modelDef.tableName && typeof modelDef.tableName !== 'string')
+      throw new InvalidArgumentError(
+        'The provided option "tableName" of the model %s ' +
+          'should be a String, but %s given.',
+        modelDef.name,
+        modelDef.tableName,
+      );
+    if (modelDef.properties) {
+      if (
+        typeof modelDef.properties !== 'object' ||
+        Array.isArray(modelDef.properties)
+      ) {
+        throw new InvalidArgumentError(
+          'The provided option "properties" of the model %s ' +
+            'should be an Object, but %s given.',
+          modelDef.name,
+          modelDef.properties,
+        );
+      }
+      this.get(PropertiesDefinitionValidator).validate(
+        modelDef.name,
+        modelDef.properties,
+      );
+    }
+    if (modelDef.relations) {
+      if (
+        typeof modelDef.relations !== 'object' ||
+        Array.isArray(modelDef.relations)
+      ) {
+        throw new InvalidArgumentError(
+          'The provided option "relations" of the model %s ' +
+            'should be an Object, but %s given.',
+          modelDef.name,
+          modelDef.relations,
+        );
+      }
+      this.get(RelationsDefinitionValidator).validate(
+        modelDef.name,
+        modelDef.relations,
+      );
+    }
+  }
+}

+ 150 - 0
src/definition/model/model-definition-validator.spec.js

@@ -0,0 +1,150 @@
+import chai from 'chai';
+import {format} from 'util';
+import {expect} from 'chai';
+import {RelationsDefinitionValidator} from './relations/index.js';
+import {PropertiesDefinitionValidator} from './properties/index.js';
+import {ModelDefinitionValidator} from './model-definition-validator.js';
+
+const S = new ModelDefinitionValidator();
+const sandbox = chai.spy.sandbox();
+
+describe('ModelDefinitionValidator', function () {
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  describe('validate', function () {
+    it('requires the given definition to be an object', function () {
+      const validate = value => () => S.validate(value);
+      const error = value =>
+        format(
+          'The model definition should be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate({name: 'model'})();
+    });
+
+    it('requires the option "name" as a non-empty string', function () {
+      const validate = name => () => S.validate({name});
+      const error = value =>
+        format(
+          'The model definition requires the option "name" ' +
+            'as a non-empty String, but %s given.',
+          value,
+        );
+      expect(validate('')).to.throw(error('""'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate('model')();
+    });
+
+    it('expects the provided option "datasource" to be a string', function () {
+      const validate = datasource => () =>
+        S.validate({name: 'model', datasource});
+      const error = value =>
+        format(
+          'The provided option "datasource" of the model "model" ' +
+            'should be a String, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate('datasource')();
+    });
+
+    it('expects the provided option "base" to be a string', function () {
+      const validate = base => () => S.validate({name: 'model', base});
+      const error = value =>
+        format(
+          'The provided option "base" of the model "model" ' +
+            'should be a String, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate('base')();
+    });
+
+    it('expects the provided option "tableName" to be a string', function () {
+      const validate = tableName => () =>
+        S.validate({name: 'model', tableName});
+      const error = value =>
+        format(
+          'The provided option "tableName" of the model "model" ' +
+            'should be a String, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate('tableName')();
+    });
+
+    it('expects the provided option "properties" to be an object', function () {
+      const validate = properties => () =>
+        S.validate({name: 'model', properties});
+      const error = value =>
+        format(
+          'The provided option "properties" of the model "model" ' +
+            'should be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      validate({})();
+    });
+
+    it('expects the provided option "relations" to be an object', function () {
+      const validate = relations => () =>
+        S.validate({name: 'model', relations});
+      const error = value =>
+        format(
+          'The provided option "relations" of the model "model" ' +
+            'should be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      validate({})();
+    });
+
+    it('uses PropertiesDefinitionValidator service to validate model properties', function () {
+      const V = S.get(PropertiesDefinitionValidator);
+      sandbox.on(V, 'validate');
+      const properties = {};
+      S.validate({name: 'model', properties});
+      expect(V.validate).to.have.been.called.once;
+      expect(V.validate).to.have.been.called.with.exactly('model', properties);
+    });
+
+    it('uses RelationsDefinitionValidator service to validate model relations', function () {
+      const V = S.get(RelationsDefinitionValidator);
+      sandbox.on(V, 'validate');
+      const relations = {};
+      S.validate({name: 'model', relations});
+      expect(V.validate).to.have.been.called.once;
+      expect(V.validate).to.have.been.called.with.exactly('model', relations);
+    });
+  });
+});

+ 20 - 0
src/definition/model/properties/data-type.js

@@ -0,0 +1,20 @@
+/**
+ * Data type.
+ *
+ * @type {{
+ *   NUMBER: string,
+ *   ARRAY: string,
+ *   STRING: string,
+ *   ANY: string,
+ *   OBJECT: string,
+ *   BOOLEAN: string,
+ * }}
+ */
+export const DataType = {
+  ANY: 'any',
+  STRING: 'string',
+  NUMBER: 'number',
+  BOOLEAN: 'boolean',
+  ARRAY: 'array',
+  OBJECT: 'object',
+};

+ 53 - 0
src/definition/model/properties/default-values-definition-validator.js

@@ -0,0 +1,53 @@
+import {Service} from '../../../service/index.js';
+import {InvalidArgumentError} from '../../../errors/index.js';
+import {ModelDataValidator} from '../model-data-validator.js';
+
+/**
+ * Default values definition validator.
+ */
+export class DefaultValuesDefinitionValidator extends Service {
+  /**
+   * Validate.
+   *
+   * @param modelName
+   * @param propDefs
+   */
+  validate(modelName, propDefs) {
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'A first argument of DefaultValuesDefinitionValidator.validate ' +
+          'should be a non-empty String, but %s given.',
+        modelName,
+      );
+    if (!propDefs || typeof propDefs !== 'object' || Array.isArray(propDefs))
+      throw new InvalidArgumentError(
+        'The provided option "properties" of the model %s ' +
+          'should be an Object, but %s given.',
+        modelName,
+        propDefs,
+      );
+    Object.keys(propDefs).forEach(propName => {
+      const propDef = propDefs[propName];
+      if (typeof propDef === 'string') return;
+      if (!('default' in propDef)) return;
+      const propValue =
+        propDef.default instanceof Function
+          ? propDef.default()
+          : propDef.default;
+      try {
+        this.get(ModelDataValidator).validatePropertyValue(
+          modelName,
+          propName,
+          propDef,
+          propValue,
+        );
+      } catch (error) {
+        if (error instanceof InvalidArgumentError)
+          throw new InvalidArgumentError(
+            `A default value is invalid. ${error.message}`,
+          );
+        throw error;
+      }
+    });
+  }
+}

+ 136 - 0
src/definition/model/properties/default-values-definition-validator.spec.js

@@ -0,0 +1,136 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {DataType} from './data-type.js';
+import {DefaultValuesDefinitionValidator} from './default-values-definition-validator.js';
+
+const S = new DefaultValuesDefinitionValidator();
+
+describe('DefaultValuesDefinitionValidator', function () {
+  describe('validate', function () {
+    it('requires a first argument to be a non-empty string', function () {
+      const validate = value => () => S.validate(value, {});
+      const error = value =>
+        format(
+          'A first argument of DefaultValuesDefinitionValidator.validate ' +
+            'should be a non-empty String, but %s given.',
+          value,
+        );
+      expect(validate('')).to.throw(error('""'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate('model')();
+    });
+
+    it('requires a second argument to be an object', function () {
+      const validate = value => () => S.validate('model', value);
+      const error = value =>
+        format(
+          'The provided option "properties" of the model "model" ' +
+            'should be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate({})();
+    });
+
+    it('does not throw an error if no properties defined', function () {
+      S.validate('model', {});
+    });
+
+    it('does not throw an error if no default value specified for a required property', function () {
+      S.validate('model', {
+        foo: {
+          type: DataType.STRING,
+          required: true,
+        },
+      });
+    });
+
+    it('does not throw an error if a default value matches a property type', function () {
+      S.validate('model', {
+        foo: {
+          type: DataType.BOOLEAN,
+          default: false,
+        },
+      });
+    });
+
+    it('does not throw an error if a default value from a factory function matches a property type', function () {
+      S.validate('model', {
+        foo: {
+          type: DataType.BOOLEAN,
+          default: () => false,
+        },
+      });
+    });
+
+    it('throws an error if a default value does not match a property type', function () {
+      const throwable = () =>
+        S.validate('model', {
+          foo: {
+            type: DataType.STRING,
+            default: 10,
+          },
+        });
+      expect(throwable).to.throw(
+        'A default value is invalid. The property "foo" of the model ' +
+          '"model" must have a String, but Number given.',
+      );
+    });
+
+    it('throws an error if a default value from a factory function does not match a property type', function () {
+      const throwable = () =>
+        S.validate('model', {
+          foo: {
+            type: DataType.STRING,
+            default: () => 10,
+          },
+        });
+      expect(throwable).to.throw(
+        'A default value is invalid. The property "foo" of the model ' +
+          '"model" must have a String, but Number given.',
+      );
+    });
+
+    it('throws an error if an array element of a default value does not match an item type', function () {
+      const throwable = () =>
+        S.validate('model', {
+          foo: {
+            type: DataType.ARRAY,
+            itemType: DataType.STRING,
+            default: [10],
+          },
+        });
+      expect(throwable).to.throw(
+        'A default value is invalid. The array property "foo" of the model "model" ' +
+          'must have a String element, but Number given.',
+      );
+    });
+
+    it('throws an error if an array element from a default value factory does not match an item type', function () {
+      const throwable = () =>
+        S.validate('model', {
+          foo: {
+            type: DataType.ARRAY,
+            itemType: DataType.STRING,
+            default: () => [10],
+          },
+        });
+      expect(throwable).to.throw(
+        'A default value is invalid. The array property "foo" of the model "model" ' +
+          'must have a String element, but Number given.',
+      );
+    });
+  });
+});

+ 4 - 0
src/definition/model/properties/index.js

@@ -0,0 +1,4 @@
+export * from './data-type.js';
+export * from './properties-definition-validator.js';
+export * from './primary-keys-definition-validator.js';
+export * from './default-values-definition-validator.js';

+ 55 - 0
src/definition/model/properties/primary-keys-definition-validator.js

@@ -0,0 +1,55 @@
+import {Service} from '../../../service/index.js';
+import {InvalidArgumentError} from '../../../errors/index.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../model-definition-utils.js';
+
+/**
+ * Primary keys definition validator.
+ */
+export class PrimaryKeysDefinitionValidator extends Service {
+  /**
+   * Validate.
+   *
+   * @param modelName
+   * @param propDefs
+   */
+  validate(modelName, propDefs) {
+    const propNames = Object.keys(propDefs).filter(propName => {
+      const propDef = propDefs[propName];
+      return propDef && typeof propDef === 'object' && propDef.primaryKey;
+    });
+    if (propNames.length < 1) {
+      const isDefaultPrimaryKeyAlreadyInUse =
+        Object.keys(propDefs).includes(DEF_PK);
+      if (isDefaultPrimaryKeyAlreadyInUse)
+        throw new InvalidArgumentError(
+          'The property name %s of the model %s is defined as a regular property. ' +
+            'In this case, a primary key should be defined explicitly. ' +
+            'Do use the option "primaryKey" to specify the primary key.',
+          DEF_PK,
+          modelName,
+        );
+      return;
+    }
+    if (propNames.length > 1)
+      throw new InvalidArgumentError(
+        'The model definition %s should not have ' +
+          'multiple primary keys, but %s keys given.',
+        modelName,
+        propNames.length,
+      );
+    const pkPropName = propNames[0];
+    const pkPropDef = propDefs[pkPropName];
+    if (
+      pkPropDef &&
+      typeof pkPropDef === 'object' &&
+      pkPropDef.default !== undefined
+    ) {
+      throw new InvalidArgumentError(
+        'Do not specify a default value for the ' +
+          'primary key %s of the model %s.',
+        pkPropName,
+        modelName,
+      );
+    }
+  }
+}

+ 145 - 0
src/definition/model/properties/primary-keys-definition-validator.spec.js

@@ -0,0 +1,145 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {DataType} from './data-type.js';
+import {PrimaryKeysDefinitionValidator} from './primary-keys-definition-validator.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../model-definition-utils.js';
+
+const S = new PrimaryKeysDefinitionValidator();
+
+describe('PrimaryKeysDefinitionValidator', function () {
+  describe('validate', function () {
+    it('does not throw an error if no primary keys provided in case of a short property definition', function () {
+      S.validate('model', {
+        foo: DataType.STRING,
+        bar: DataType.NUMBER,
+      });
+    });
+
+    it('does not throw an error if no primary keys provided in case of a full property definition', function () {
+      S.validate('model', {
+        foo: {
+          type: DataType.STRING,
+          default: 'string',
+        },
+        bar: {
+          type: DataType.NUMBER,
+          default: 10,
+        },
+      });
+    });
+
+    it('throws an error if a model definition has a multiple primary keys', function () {
+      const throwable = () =>
+        S.validate('model', {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        });
+      expect(throwable).to.throw(
+        'The model definition "model" should not have ' +
+          'multiple primary keys, but 2 keys given.',
+      );
+    });
+
+    it('throws an error if a default primary key is already in use as a regular property in case of a short property definition', function () {
+      const throwable = () =>
+        S.validate('model', {
+          [DEF_PK]: DataType.NUMBER,
+        });
+      expect(throwable).to.throw(
+        format(
+          'The property name "%s" of the model "model" is defined as a regular property. ' +
+            'In this case, a primary key should be defined explicitly. ' +
+            'Do use the option "primaryKey" to specify the primary key.',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('throws an error if a default primary key is already in use as a regular property in case of a full property definition', function () {
+      const throwable = () =>
+        S.validate('model', {
+          [DEF_PK]: {
+            type: DataType.NUMBER,
+          },
+        });
+      expect(throwable).to.throw(
+        format(
+          'The property name "%s" of the model "model" is defined as a regular property. ' +
+            'In this case, a primary key should be defined explicitly. ' +
+            'Do use the option "primaryKey" to specify the primary key.',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('throws an error if a default primary key is already in use as a regular property with a default value', function () {
+      const throwable = () =>
+        S.validate('model', {
+          [DEF_PK]: {
+            type: DataType.NUMBER,
+            default: 10,
+          },
+        });
+      expect(throwable).to.throw(
+        format(
+          'The property name "%s" of the model "model" is defined as a regular property. ' +
+            'In this case, a primary key should be defined explicitly. ' +
+            'Do use the option "primaryKey" to specify the primary key.',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('throws an error if a default primary key defined explicitly and it has a default value', function () {
+      const throwable = () =>
+        S.validate('model', {
+          [DEF_PK]: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            default: 10,
+          },
+        });
+      expect(throwable).to.throw(
+        format(
+          'Do not specify a default value for the ' +
+            'primary key "%s" of the model "model".',
+          DEF_PK,
+        ),
+      );
+    });
+
+    it('does not throw an error if a default primary key is defined explicitly', function () {
+      S.validate('model', {
+        [DEF_PK]: {
+          type: DataType.NUMBER,
+          primaryKey: true,
+        },
+      });
+    });
+
+    it('does not throw an error if a primary key has a custom name', function () {
+      S.validate('model', {
+        myId: {
+          type: DataType.NUMBER,
+          primaryKey: true,
+        },
+      });
+    });
+
+    it('does not throw an error if a primary key has a custom name and a default primary key is used as a regular property', function () {
+      S.validate('model', {
+        myId: {
+          type: DataType.NUMBER,
+          primaryKey: true,
+        },
+        [DEF_PK]: DataType.STRING,
+      });
+    });
+  });
+});

+ 189 - 0
src/definition/model/properties/properties-definition-validator.js

@@ -0,0 +1,189 @@
+import {DataType as Type} from './data-type.js';
+import {Service} from '../../../service/index.js';
+import {arrayToString} from '../../../utils/index.js';
+import {InvalidArgumentError} from '../../../errors/index.js';
+import {PrimaryKeysDefinitionValidator} from './primary-keys-definition-validator.js';
+import {DefaultValuesDefinitionValidator} from './default-values-definition-validator.js';
+
+/**
+ * Properties definition validator.
+ */
+export class PropertiesDefinitionValidator extends Service {
+  /**
+   * Validate.
+   *
+   * @param modelName
+   * @param propDefs
+   */
+  validate(modelName, propDefs) {
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'A first argument of PropertiesDefinitionValidator.validate ' +
+          'should be a non-empty String, but %s given.',
+        modelName,
+      );
+    if (!propDefs || typeof propDefs !== 'object' || Array.isArray(propDefs)) {
+      throw new InvalidArgumentError(
+        'The provided option "properties" of the model %s ' +
+          'should be an Object, but %s given.',
+        modelName,
+        propDefs,
+      );
+    }
+    const propNames = Object.keys(propDefs);
+    propNames.forEach(propName => {
+      const propDef = propDefs[propName];
+      this._validateProperty(modelName, propName, propDef);
+    });
+    this.get(PrimaryKeysDefinitionValidator).validate(modelName, propDefs);
+    this.get(DefaultValuesDefinitionValidator).validate(modelName, propDefs);
+  }
+
+  /**
+   * Validate property.
+   *
+   * @param modelName
+   * @param propName
+   * @param propDef
+   */
+  _validateProperty(modelName, propName, propDef) {
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'A first argument of PropertiesDefinitionValidator._validateProperty ' +
+          'should be a non-empty String, but %s given.',
+        modelName,
+      );
+    if (!propName || typeof propName !== 'string')
+      throw new InvalidArgumentError(
+        'The property name of the model %s should be ' +
+          'a non-empty String, but %s given.',
+        modelName,
+        propName,
+      );
+    if (!propDef)
+      throw new InvalidArgumentError(
+        'The property %s of the model %s should have ' +
+          'a property definition, but %s given.',
+        propName,
+        modelName,
+        propDef,
+      );
+    if (typeof propDef === 'string') {
+      if (!Object.values(Type).includes(propDef))
+        throw new InvalidArgumentError(
+          'In case of a short property definition, the property %s ' +
+            'of the model %s should have one of data types: %s, but %s given.',
+          propName,
+          modelName,
+          new String(arrayToString(Object.values(Type))),
+          propDef,
+        );
+      return;
+    }
+    if (!propDef || typeof propDef !== 'object' || Array.isArray(propDef)) {
+      throw new InvalidArgumentError(
+        'In case of a full property definition, the property %s ' +
+          'of the model %s should be an Object, but %s given.',
+        propName,
+        modelName,
+        propDef,
+      );
+    }
+    if (!propDef.type || !Object.values(Type).includes(propDef.type))
+      throw new InvalidArgumentError(
+        'The property %s of the model %s requires the option "type" ' +
+          'to have one of data types: %s, but %s given.',
+        propName,
+        modelName,
+        new String(arrayToString(Object.values(Type))),
+        propDef.type,
+      );
+    if (propDef.itemType && !Object.values(Type).includes(propDef.itemType)) {
+      throw new InvalidArgumentError(
+        'The provided option "itemType" of the property %s in the model %s ' +
+          'should have one of data types: %s, but %s given.',
+        propName,
+        modelName,
+        new String(arrayToString(Object.values(Type))),
+        propDef.itemType,
+      );
+    }
+    if (propDef.model && typeof propDef.model !== 'string')
+      throw new InvalidArgumentError(
+        'The provided option "model" of the property %s in the model %s ' +
+          'should be a String, but %s given.',
+        propName,
+        modelName,
+        propDef.model,
+      );
+    if (propDef.primaryKey && typeof propDef.primaryKey !== 'boolean')
+      throw new InvalidArgumentError(
+        'The provided option "primaryKey" of the property %s in the model %s ' +
+          'should be a Boolean, but %s given.',
+        propName,
+        modelName,
+        propDef.primaryKey,
+      );
+    if (propDef.columnName && typeof propDef.columnName !== 'string')
+      throw new InvalidArgumentError(
+        'The provided option "columnName" of the property %s in the model %s ' +
+          'should be a String, but %s given.',
+        propName,
+        modelName,
+        propDef.columnName,
+      );
+    if (propDef.columnType && typeof propDef.columnType !== 'string')
+      throw new InvalidArgumentError(
+        'The provided option "columnType" of the property %s in the model %s ' +
+          'should be a String, but %s given.',
+        propName,
+        modelName,
+        propDef.columnType,
+      );
+    if (propDef.required && typeof propDef.required !== 'boolean')
+      throw new InvalidArgumentError(
+        'The provided option "required" of the property %s in the model %s ' +
+          'should be a Boolean, but %s given.',
+        propName,
+        modelName,
+        propDef.required,
+      );
+    if (propDef.required && propDef.default !== undefined)
+      throw new InvalidArgumentError(
+        'The property %s of the model %s is a required property, ' +
+          'so it should not have the option "default" to be provided.',
+        propName,
+        modelName,
+      );
+    if (propDef.primaryKey && propDef.required)
+      throw new InvalidArgumentError(
+        'The property %s of the model %s is a primary key, ' +
+          'so it should not have the option "required" to be provided.',
+        propName,
+        modelName,
+      );
+    if (propDef.primaryKey && propDef.default !== undefined)
+      throw new InvalidArgumentError(
+        'The property %s of the model %s is a primary key, ' +
+          'so it should not have the option "default" to be provided.',
+        propName,
+        modelName,
+      );
+    if (propDef.itemType && propDef.type !== Type.ARRAY)
+      throw new InvalidArgumentError(
+        'The property %s of the model %s has the non-array type, ' +
+          'so it should not have the option "itemType" to be provided.',
+        propName,
+        modelName,
+        propDef.type,
+      );
+    if (propDef.model && propDef.type !== Type.OBJECT)
+      throw new InvalidArgumentError(
+        'The property %s of the model %s has the non-object type, ' +
+          'so it should not have the option "model" to be provided.',
+        propName,
+        modelName,
+        propDef.type,
+      );
+  }
+}

+ 374 - 0
src/definition/model/properties/properties-definition-validator.spec.js

@@ -0,0 +1,374 @@
+import chai from 'chai';
+import {format} from 'util';
+import {expect} from 'chai';
+import {DataType} from './data-type.js';
+import {arrayToString} from '../../../utils/index.js';
+import {PropertiesDefinitionValidator} from './properties-definition-validator.js';
+import {PrimaryKeysDefinitionValidator} from './primary-keys-definition-validator.js';
+import {DefaultValuesDefinitionValidator} from './default-values-definition-validator.js';
+
+const S = new PropertiesDefinitionValidator();
+const sandbox = chai.spy.sandbox();
+
+describe('PropertiesDefinitionValidator', function () {
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  describe('validate', function () {
+    it('requires a first argument to be a non-empty string', function () {
+      const validate = value => () => S.validate(value, {});
+      const error = value =>
+        format(
+          'A first argument of PropertiesDefinitionValidator.validate ' +
+            'should be a non-empty String, but %s given.',
+          value,
+        );
+      expect(validate('')).to.throw(error('""'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate('model')();
+    });
+
+    it('requires a second argument to be an object', function () {
+      const validate = value => () => S.validate('model', value);
+      const error = value =>
+        format(
+          'The provided option "properties" of the model "model" ' +
+            'should be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate({})();
+    });
+
+    it('requires a property name as a non-empty string', function () {
+      const validate = properties => () => S.validate('model', properties);
+      const error = value =>
+        format(
+          'The property name of the model "model" should be ' +
+            'a non-empty String, but %s given.',
+          value,
+        );
+      expect(validate({['']: {}})).to.throw(error('""'));
+      validate({foo: DataType.STRING})();
+    });
+
+    it('requires a property definition', function () {
+      const validate = foo => () => S.validate('model', {foo});
+      const error = value =>
+        format(
+          'The property "foo" of the model "model" should have ' +
+            'a property definition, but %s given.',
+          value,
+        );
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate(DataType.STRING)();
+      validate({type: DataType.STRING})();
+    });
+
+    it('expects a short property definition to be DataType', function () {
+      const validate = foo => () => S.validate('model', {foo});
+      const error = value =>
+        format(
+          'In case of a short property definition, the property "foo" ' +
+            'of the model "model" should have one of data types: %s, but %s given.',
+          arrayToString(Object.values(DataType)),
+          value,
+        );
+      expect(validate('invalid')).to.throw(error('"invalid"'));
+      validate(DataType.STRING)();
+    });
+
+    it('expects a full property definition to be an object', function () {
+      const validate = foo => () => S.validate('model', {foo});
+      const error = value =>
+        format(
+          'In case of a full property definition, the property "foo" ' +
+            'of the model "model" should be an Object, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      validate({type: DataType.STRING})();
+    });
+
+    it('requires the option "type" to be a DataType', function () {
+      const validate = type => () => S.validate('model', {foo: {type}});
+      const error = value =>
+        format(
+          'The property "foo" of the model "model" requires the option "type" ' +
+            'to have one of data types: %s, but %s given.',
+          arrayToString(Object.values(DataType)),
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate(DataType.STRING)();
+    });
+
+    it('expects provided the option "itemType" to be a DataType', function () {
+      const validate = itemType => {
+        const foo = {type: DataType.ARRAY, itemType};
+        return () => S.validate('model', {foo});
+      };
+      const error = value =>
+        format(
+          'The provided option "itemType" of the property "foo" in the model "model" ' +
+            'should have one of data types: %s, but %s given.',
+          arrayToString(Object.values(DataType)),
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate(DataType.STRING)();
+    });
+
+    it('expects provided the option "model" to be a string', function () {
+      const validate = model => {
+        const foo = {
+          type: DataType.OBJECT,
+          model,
+        };
+        return () => S.validate('model', {foo});
+      };
+      const error = value =>
+        format(
+          'The provided option "model" of the property "foo" in the model "model" ' +
+            'should be a String, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate('model')();
+    });
+
+    it('expects provided the option "primaryKey" to be a boolean', function () {
+      const validate = primaryKey => {
+        const foo = {
+          type: DataType.STRING,
+          primaryKey,
+        };
+        return () => S.validate('model', {foo});
+      };
+      const error = value =>
+        format(
+          'The provided option "primaryKey" of the property "foo" in the model "model" ' +
+            'should be a Boolean, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate(true)();
+      validate(false)();
+    });
+
+    it('expects provided the option "columnName" to be a string', function () {
+      const validate = columnName => {
+        const foo = {
+          type: DataType.STRING,
+          columnName,
+        };
+        return () => S.validate('model', {foo});
+      };
+      const error = value =>
+        format(
+          'The provided option "columnName" of the property "foo" in the model "model" ' +
+            'should be a String, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate('columnName')();
+    });
+
+    it('expects provided the option "columnType" to be a string', function () {
+      const validate = columnType => {
+        const foo = {
+          type: DataType.STRING,
+          columnType,
+        };
+        return () => S.validate('model', {foo});
+      };
+      const error = value =>
+        format(
+          'The provided option "columnType" of the property "foo" in the model "model" ' +
+            'should be a String, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate('columnType')();
+    });
+
+    it('expects provided the option "required" to be a boolean', function () {
+      const validate = required => {
+        const foo = {
+          type: DataType.STRING,
+          required,
+        };
+        return () => S.validate('model', {foo});
+      };
+      const error = value =>
+        format(
+          'The provided option "required" of the property "foo" in the model "model" ' +
+            'should be a Boolean, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      validate(true)();
+      validate(false)();
+    });
+
+    it('expects the required property should not have the option "default" to be provided', function () {
+      const validate = value => () => {
+        const foo = {
+          type: DataType.ANY,
+          required: true,
+          default: value,
+        };
+        S.validate('model', {foo});
+      };
+      const error = format(
+        'The property "foo" of the model "model" is a required property, ' +
+          'so it should not have the option "default" to be provided.',
+      );
+      expect(validate('str')).to.throw(error);
+      expect(validate(10)).to.throw(error);
+      expect(validate(true)).to.throw(error);
+      expect(validate(false)).to.throw(error);
+      expect(validate([])).to.throw(error);
+      expect(validate({})).to.throw(error);
+      expect(validate(null)).to.throw(error);
+      validate(undefined);
+    });
+
+    it('expects the primary key should not have the option "required" to be true', function () {
+      const validate = required => () => {
+        const foo = {
+          type: DataType.ANY,
+          primaryKey: true,
+          required,
+        };
+        S.validate('model', {foo});
+      };
+      const error = format(
+        'The property "foo" of the model "model" is a primary key, ' +
+          'so it should not have the option "required" to be provided.',
+      );
+      expect(validate(true)).to.throw(error);
+      validate(false);
+      validate(undefined);
+    });
+
+    it('expects the primary key should not have the option "default" to be provided', function () {
+      const validate = value => () => {
+        const foo = {
+          type: DataType.ANY,
+          primaryKey: true,
+          default: value,
+        };
+        S.validate('model', {foo});
+      };
+      const error = format(
+        'The property "foo" of the model "model" is a primary key, ' +
+          'so it should not have the option "default" to be provided.',
+      );
+      expect(validate('str')).to.throw(error);
+      expect(validate(10)).to.throw(error);
+      expect(validate(true)).to.throw(error);
+      expect(validate(false)).to.throw(error);
+      expect(validate([])).to.throw(error);
+      expect(validate({})).to.throw(error);
+      expect(validate(null)).to.throw(error);
+      validate(undefined);
+    });
+
+    it('expects a non-array property should not have the option "itemType" to be provided', function () {
+      const validate = type => () => {
+        const foo = {
+          type,
+          itemType: DataType.STRING,
+        };
+        S.validate('model', {foo});
+      };
+      const error =
+        'The property "foo" of the model "model" has the non-array type, ' +
+        'so it should not have the option "itemType" to be provided.';
+      expect(validate(DataType.ANY)).to.throw(error);
+      expect(validate(DataType.STRING)).to.throw(error);
+      expect(validate(DataType.NUMBER)).to.throw(error);
+      expect(validate(DataType.BOOLEAN)).to.throw(error);
+      expect(validate(DataType.OBJECT)).to.throw(error);
+      validate(DataType.ARRAY);
+    });
+
+    it('expects a non-object property should not have the option "model" to be provided', function () {
+      const validate = type => () => {
+        const foo = {
+          type,
+          model: 'model',
+        };
+        S.validate('model', {foo});
+      };
+      const error =
+        'The property "foo" of the model "model" has the non-object type, ' +
+        'so it should not have the option "model" to be provided.';
+      expect(validate(DataType.ANY)).to.throw(error);
+      expect(validate(DataType.STRING)).to.throw(error);
+      expect(validate(DataType.NUMBER)).to.throw(error);
+      expect(validate(DataType.BOOLEAN)).to.throw(error);
+      expect(validate(DataType.ARRAY)).to.throw(error);
+      validate(DataType.OBJECT);
+    });
+
+    it('uses PrimaryKeysDefinitionValidator to validate primary keys', function () {
+      const V = S.get(PrimaryKeysDefinitionValidator);
+      sandbox.on(V, 'validate');
+      const propDefs = {};
+      S.validate('model', propDefs);
+      expect(V.validate).to.have.been.called.once;
+      expect(V.validate).to.have.been.called.with.exactly('model', propDefs);
+    });
+
+    it('uses DefaultValuesDefinitionValidator to validate default values', function () {
+      const V = S.get(DefaultValuesDefinitionValidator);
+      sandbox.on(V, 'validate');
+      const propDefs = {};
+      S.validate('model', propDefs);
+      expect(V.validate).to.have.been.called.once;
+      expect(V.validate).to.have.been.called.with.exactly('model', propDefs);
+    });
+  });
+});

+ 2 - 0
src/definition/model/relations/index.js

@@ -0,0 +1,2 @@
+export * from './relation-type.js';
+export * from './relations-definition-validator.js';

+ 16 - 0
src/definition/model/relations/relation-type.js

@@ -0,0 +1,16 @@
+/**
+ * Relation type.
+ *
+ * @type {{
+ *   BELONGS_TO: string,
+ *   HAS_ONE: string,
+ *   HAS_MANY: string,
+ *   REFERENCES_MANY: string,
+ * }}
+ */
+export const RelationType = {
+  BELONGS_TO: 'belongsTo',
+  HAS_ONE: 'hasOne',
+  HAS_MANY: 'hasMany',
+  REFERENCES_MANY: 'referencesMany',
+};

+ 450 - 0
src/definition/model/relations/relations-definition-validator.js

@@ -0,0 +1,450 @@
+import {RelationType} from './relation-type.js';
+import {Service} from '../../../service/index.js';
+import {arrayToString} from '../../../utils/index.js';
+import {RelationType as Type} from './relation-type.js';
+import {InvalidArgumentError} from '../../../errors/index.js';
+
+/**
+ * Relations definition validator.
+ */
+export class RelationsDefinitionValidator extends Service {
+  /**
+   * Validate.
+   *
+   * @param modelName
+   * @param relDefs
+   */
+  validate(modelName, relDefs) {
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'A first argument of RelationsDefinitionValidator.validate ' +
+          'should be a non-empty String, but %s given.',
+        modelName,
+      );
+    if (!relDefs || typeof relDefs !== 'object' || Array.isArray(relDefs))
+      throw new InvalidArgumentError(
+        'The provided option "relations" of the model %s ' +
+          'should be an Object, but %s given.',
+        modelName,
+        relDefs,
+      );
+    const relNames = Object.keys(relDefs);
+    relNames.forEach(relName => {
+      const relDef = relDefs[relName];
+      this._validateRelation(modelName, relName, relDef);
+    });
+  }
+
+  /**
+   * Validate relation.
+   *
+   * @param modelName
+   * @param relName
+   * @param relDef
+   */
+  _validateRelation(modelName, relName, relDef) {
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'A first argument of RelationsDefinitionValidator._validateRelation ' +
+          'should be a non-empty String, but %s given.',
+        modelName,
+      );
+    if (!relName || typeof relName !== 'string')
+      throw new InvalidArgumentError(
+        'The relation name of the model %s should be ' +
+          'a non-empty String, but %s given.',
+        modelName,
+        relName,
+      );
+    if (!relDef || typeof relDef !== 'object' || Array.isArray(relDef))
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s should be an Object, but %s given.',
+        relName,
+        modelName,
+        relDef,
+      );
+    if (!relDef.type || !Object.values(Type).includes(relDef.type))
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s requires the option "type" ' +
+          'to have one of relation types: %s, but %s given.',
+        relName,
+        modelName,
+        new String(arrayToString(Object.values(Type))),
+        relDef.type,
+      );
+    this._validateBelongsTo(modelName, relName, relDef);
+    this._validateHasOne(modelName, relName, relDef);
+    this._validateHasMany(modelName, relName, relDef);
+    this._validateReferencesMany(modelName, relName, relDef);
+  }
+
+  /**
+   * Validate belongs to.
+   *
+   * @example A regular "belongsTo" relation.
+   * ```
+   * {
+   *   type: RelationType.BELONGS_TO,
+   *   model: 'model',
+   *   foreignKey: 'modelId', // optional
+   * }
+   * ```
+   *
+   * @example A polymorphic "belongsTo" relation.
+   * ```
+   * {
+   *   type: RelationType.BELONGS_TO,
+   *   polymorphic: true,
+   *   foreignKey: 'referenceId',     // optional
+   *   discriminator: 'referenceType, // optional
+   * }
+   * ```
+   *
+   * @param {string} modelName
+   * @param {string} relName
+   * @param {Record<string, unknown>} relDef
+   * @private
+   */
+  _validateBelongsTo(modelName, relName, relDef) {
+    if (relDef.type !== Type.BELONGS_TO) return;
+    if (relDef.polymorphic) {
+      // A polymorphic "belongsTo" relation.
+      if (typeof relDef.polymorphic !== 'boolean')
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s has the type "belongsTo", ' +
+            'so it expects the option "polymorphic" to be a Boolean, ' +
+            'but %s given.',
+          relName,
+          modelName,
+          relDef.polymorphic,
+        );
+      if (relDef.foreignKey && typeof relDef.foreignKey !== 'string')
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s is a polymorphic "belongsTo" relation, ' +
+            'so it expects the provided option "foreignKey" to be a String, ' +
+            'but %s given.',
+          relName,
+          modelName,
+          relDef.foreignKey,
+        );
+      if (relDef.discriminator && typeof relDef.discriminator !== 'string')
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s is a polymorphic "belongsTo" relation, ' +
+            'so it expects the provided option "discriminator" to be a String, ' +
+            'but %s given.',
+          relName,
+          modelName,
+          relDef.discriminator,
+        );
+    } else {
+      // A regular "belongsTo" relation.
+      if (!relDef.model || typeof relDef.model !== 'string')
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s has the type "belongsTo", ' +
+            'so it requires the option "model" to be a non-empty String, ' +
+            'but %s given.',
+          relName,
+          modelName,
+          relDef.model,
+        );
+      if (relDef.foreignKey && typeof relDef.foreignKey !== 'string')
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s has the type "belongsTo", ' +
+            'so it expects the provided option "foreignKey" to be a String, ' +
+            'but %s given.',
+          relName,
+          modelName,
+          relDef.foreignKey,
+        );
+      if (relDef.discriminator)
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s is a non-polymorphic "belongsTo" relation, ' +
+            'so it should not have the option "discriminator" to be provided.',
+          relName,
+          modelName,
+        );
+    }
+  }
+
+  /**
+   * Validate has one.
+   *
+   * @example A regular "hasOne" relation.
+   * ```
+   * {
+   *   type: RelationType.HAS_ONE,
+   *   model: 'model',
+   *   foreignKey: 'modelId',
+   * }
+   * ```
+   *
+   * @example A polymorphic "hasOne" relation with a target relation name.
+   * ```
+   * {
+   *   type: RelationType.HAS_ONE,
+   *   model: 'model',
+   *   polymorphic: 'reference',
+   * }
+   * ```
+   *
+   * @example A polymorphic "hasOne" relation with target relation keys.
+   * ```
+   * {
+   *   type: RelationType.HAS_ONE,
+   *   model: 'model',
+   *   polymorphic: true,
+   *   foreignKey: 'referenceId',
+   *   discriminator: 'referenceType,
+   * }
+   * ```
+   *
+   * @param {string} modelName
+   * @param {string} relName
+   * @param {Record<string, unknown>} relDef
+   * @private
+   */
+  _validateHasOne(modelName, relName, relDef) {
+    if (relDef.type !== RelationType.HAS_ONE) return;
+    if (!relDef.model || typeof relDef.model !== 'string')
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s has the type "hasOne", ' +
+          'so it requires the option "model" to be a non-empty String, ' +
+          'but %s given.',
+        relName,
+        modelName,
+        relDef.model,
+      );
+    if (relDef.polymorphic) {
+      if (typeof relDef.polymorphic === 'string') {
+        // A polymorphic "hasOne" relation with a target relation name.
+        if (relDef.foreignKey)
+          throw new InvalidArgumentError(
+            'The relation %s of the model %s has the option "polymorphic" with ' +
+              'a String value, so it should not have the option "foreignKey" ' +
+              'to be provided.',
+            relName,
+            modelName,
+          );
+        if (relDef.discriminator)
+          throw new InvalidArgumentError(
+            'The relation %s of the model %s has the option "polymorphic" with ' +
+              'a String value, so it should not have the option "discriminator" ' +
+              'to be provided.',
+            relName,
+            modelName,
+          );
+      } else if (typeof relDef.polymorphic === 'boolean') {
+        // A polymorphic "hasOne" relation with target relation keys.
+        if (!relDef.foreignKey || typeof relDef.foreignKey !== 'string')
+          throw new InvalidArgumentError(
+            'The relation %s of the model %s has the option "polymorphic" ' +
+              'with "true" value, so it requires the option "foreignKey" ' +
+              'to be a non-empty String, but %s given.',
+            relName,
+            modelName,
+            relDef.foreignKey,
+          );
+        if (!relDef.discriminator || typeof relDef.discriminator !== 'string')
+          throw new InvalidArgumentError(
+            'The relation %s of the model %s has the option "polymorphic" ' +
+              'with "true" value, so it requires the option "discriminator" ' +
+              'to be a non-empty String, but %s given.',
+            relName,
+            modelName,
+            relDef.discriminator,
+          );
+      } else {
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s has the type "hasOne", ' +
+            'so it expects the provided option "polymorphic" to be ' +
+            'a String or a Boolean, but %s given.',
+          relName,
+          modelName,
+          relDef.polymorphic,
+        );
+      }
+    } else {
+      // A regular "hasOne" relation.
+      if (!relDef.foreignKey || typeof relDef.foreignKey !== 'string')
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s has the type "hasOne", ' +
+            'so it requires the option "foreignKey" to be a non-empty String, ' +
+            'but %s given.',
+          relName,
+          modelName,
+          relDef.foreignKey,
+        );
+      if (relDef.discriminator)
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s is a non-polymorphic "hasOne" relation, ' +
+            'so it should not have the option "discriminator" to be provided.',
+          relName,
+          modelName,
+        );
+    }
+  }
+
+  /**
+   * Validate has one.
+   *
+   * @example A regular "hasMany" relation.
+   * ```
+   * {
+   *   type: RelationType.HAS_MANY,
+   *   model: 'model',
+   *   foreignKey: 'modelId',
+   * }
+   * ```
+   *
+   * @example A polymorphic "hasMany" relation with a target relation name.
+   * ```
+   * {
+   *   type: RelationType.HAS_MANY,
+   *   model: 'model',
+   *   polymorphic: 'reference',
+   * }
+   * ```
+   *
+   * @example A polymorphic "hasMany" relation with target relation keys.
+   * ```
+   * {
+   *   type: RelationType.HAS_MANY,
+   *   model: 'model',
+   *   polymorphic: true,
+   *   foreignKey: 'referenceId',
+   *   discriminator: 'referenceType,
+   * }
+   * ```
+   *
+   * @param {string} modelName
+   * @param {string} relName
+   * @param {Record<string, unknown>} relDef
+   * @private
+   */
+  _validateHasMany(modelName, relName, relDef) {
+    if (relDef.type !== RelationType.HAS_MANY) return;
+    if (!relDef.model || typeof relDef.model !== 'string')
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s has the type "hasMany", ' +
+          'so it requires the option "model" to be a non-empty String, ' +
+          'but %s given.',
+        relName,
+        modelName,
+        relDef.model,
+      );
+    if (relDef.polymorphic) {
+      if (typeof relDef.polymorphic === 'string') {
+        // A polymorphic "hasMany" relation with a target relation name.
+        if (relDef.foreignKey)
+          throw new InvalidArgumentError(
+            'The relation %s of the model %s has the option "polymorphic" with ' +
+              'a String value, so it should not have the option "foreignKey" ' +
+              'to be provided.',
+            relName,
+            modelName,
+          );
+        if (relDef.discriminator)
+          throw new InvalidArgumentError(
+            'The relation %s of the model %s has the option "polymorphic" with ' +
+              'a String value, so it should not have the option "discriminator" ' +
+              'to be provided.',
+            relName,
+            modelName,
+          );
+      } else if (typeof relDef.polymorphic === 'boolean') {
+        // A polymorphic "hasMany" relation with target relation keys.
+        if (!relDef.foreignKey || typeof relDef.foreignKey !== 'string')
+          throw new InvalidArgumentError(
+            'The relation %s of the model %s has the option "polymorphic" ' +
+              'with "true" value, so it requires the option "foreignKey" ' +
+              'to be a non-empty String, but %s given.',
+            relName,
+            modelName,
+            relDef.foreignKey,
+          );
+        if (!relDef.discriminator || typeof relDef.discriminator !== 'string')
+          throw new InvalidArgumentError(
+            'The relation %s of the model %s has the option "polymorphic" ' +
+              'with "true" value, so it requires the option "discriminator" ' +
+              'to be a non-empty String, but %s given.',
+            relName,
+            modelName,
+            relDef.discriminator,
+          );
+      } else {
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s has the type "hasMany", ' +
+            'so it expects the provided option "polymorphic" to be ' +
+            'a String or a Boolean, but %s given.',
+          relName,
+          modelName,
+          relDef.polymorphic,
+        );
+      }
+    } else {
+      // A regular "hasMany" relation.
+      if (!relDef.foreignKey || typeof relDef.foreignKey !== 'string')
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s has the type "hasMany", ' +
+            'so it requires the option "foreignKey" to be a non-empty String, ' +
+            'but %s given.',
+          relName,
+          modelName,
+          relDef.foreignKey,
+        );
+      if (relDef.discriminator)
+        throw new InvalidArgumentError(
+          'The relation %s of the model %s is a non-polymorphic "hasMany" relation, ' +
+            'so it should not have the option "discriminator" to be provided.',
+          relName,
+          modelName,
+        );
+    }
+  }
+
+  /**
+   * Validate references many.
+   *
+   * @example
+   * ```
+   * {
+   *   type: RelationType.REFERENCES_MANY,
+   *   model: 'model',
+   *   foreignKey: 'modelIds', // optional
+   * }
+   * ```
+   *
+   * @param {string} modelName
+   * @param {string} relName
+   * @param {Record<string, unknown>} relDef
+   * @private
+   */
+  _validateReferencesMany(modelName, relName, relDef) {
+    if (relDef.type !== Type.REFERENCES_MANY) return;
+    if (!relDef.model || typeof relDef.model !== 'string')
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s has the type "referencesMany", ' +
+          'so it requires the option "model" to be a non-empty String, ' +
+          'but %s given.',
+        relName,
+        modelName,
+        relDef.model,
+      );
+    if (relDef.foreignKey && typeof relDef.foreignKey !== 'string')
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s has the type "referencesMany", ' +
+          'so it expects the provided option "foreignKey" to be a String, ' +
+          'but %s given.',
+        relName,
+        modelName,
+        relDef.foreignKey,
+      );
+    if (relDef.discriminator)
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s has the type "referencesMany", ' +
+          'so it should not have the option "discriminator" to be provided.',
+        relName,
+        modelName,
+      );
+  }
+}

+ 773 - 0
src/definition/model/relations/relations-definition-validator.spec.js

@@ -0,0 +1,773 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {RelationType} from './relation-type.js';
+import {arrayToString} from '../../../utils/index.js';
+import {RelationsDefinitionValidator} from './relations-definition-validator.js';
+
+const S = new RelationsDefinitionValidator();
+
+describe('RelationsDefinitionValidator', function () {
+  describe('validate', function () {
+    it('requires a first argument to be a non-empty string', function () {
+      const validate = value => () => S.validate(value, {});
+      const error = value =>
+        format(
+          'A first argument of RelationsDefinitionValidator.validate ' +
+            'should be a non-empty String, but %s given.',
+          value,
+        );
+      expect(validate('')).to.throw(error('""'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate('model')();
+    });
+
+    it('requires a second argument to be an object', function () {
+      const validate = value => () => S.validate('model', value);
+      const error = value =>
+        format(
+          'The provided option "relations" of the model "model" ' +
+            'should be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate({})();
+    });
+
+    it('requires a relation name to be a non-empty string', function () {
+      const validate = relations => () => S.validate('model', relations);
+      const error = value =>
+        format(
+          'The relation name of the model "model" should be ' +
+            'a non-empty String, but %s given.',
+          value,
+        );
+      expect(validate({['']: {}})).to.throw(error('""'));
+      validate({foo: {type: RelationType.BELONGS_TO, model: 'model'}})();
+    });
+
+    it('requires a relation definition to be an object', function () {
+      const validate = foo => () => S.validate('model', {foo});
+      const error = value =>
+        format(
+          'The relation "foo" of the model "model" should ' +
+            'be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate({type: RelationType.BELONGS_TO, model: 'model'})();
+    });
+
+    it('requires the option "type" to be a RelationType', function () {
+      const validate = type => () => {
+        const foo = {type, model: 'model'};
+        S.validate('model', {foo});
+      };
+      const error = value =>
+        format(
+          'The relation "foo" of the model "model" requires the option "type" ' +
+            'to have one of relation types: %s, but %s given.',
+          arrayToString(Object.values(RelationType)),
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate(false)).to.throw(error('false'));
+      expect(validate([])).to.throw(error('Array'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate(undefined)).to.throw(error('undefined'));
+      expect(validate(null)).to.throw(error('null'));
+      validate(RelationType.BELONGS_TO)();
+    });
+
+    describe('belongsTo', function () {
+      describe('a regular relation', function () {
+        it('requires the option "model" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.BELONGS_TO,
+              model: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "belongsTo", ' +
+                'so it requires the option "model" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('model')();
+        });
+
+        it('expects the provided option "foreignKey" to be a string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.BELONGS_TO,
+              model: 'model',
+              foreignKey: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "belongsTo", ' +
+                'so it expects the provided option "foreignKey" to be a String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          validate('foreignKey')();
+          validate('')();
+          validate(false)();
+          validate(undefined)();
+          validate(null)();
+        });
+
+        it('throws an error if the option "discriminator" is provided', function () {
+          const throwable = () =>
+            S.validate('model', {
+              foo: {
+                type: RelationType.BELONGS_TO,
+                model: 'model',
+                discriminator: 'referenceType',
+              },
+            });
+          expect(throwable).to.throw(
+            'The relation "foo" of the model "model" is a non-polymorphic "belongsTo" relation, ' +
+              'so it should not have the option "discriminator" to be provided.',
+          );
+        });
+      });
+
+      describe('a polymorphic relation', function () {
+        it('requires the option "polymorphic" to be a boolean', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.BELONGS_TO,
+              polymorphic: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const error = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "belongsTo", ' +
+                'so it expects the option "polymorphic" to be a Boolean, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('str')).to.throw(error('"str"'));
+          expect(validate(10)).to.throw(error('10'));
+          expect(validate([])).to.throw(error('Array'));
+          expect(validate({})).to.throw(error('Object'));
+          validate(true)();
+        });
+
+        it('expects the provided option "foreignKey" to be a string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.BELONGS_TO,
+              polymorphic: true,
+              foreignKey: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" is a polymorphic "belongsTo" relation, ' +
+                'so it expects the provided option "foreignKey" to be a String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          validate('foreignKey')();
+          validate('')();
+          validate(false)();
+          validate(undefined)();
+          validate(null)();
+        });
+
+        it('expects the provided option "discriminator" to be a string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.BELONGS_TO,
+              polymorphic: true,
+              discriminator: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" is a polymorphic "belongsTo" relation, ' +
+                'so it expects the provided option "discriminator" to be a String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          validate('discriminator')();
+          validate('')();
+          validate(false)();
+          validate(undefined)();
+          validate(null)();
+        });
+      });
+    });
+
+    describe('hasOne', function () {
+      describe('a regular relation', function () {
+        it('requires the option "model" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_ONE,
+              model: 'model',
+              foreignKey: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "hasOne", ' +
+                'so it requires the option "foreignKey" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('modelId')();
+        });
+
+        it('requires the option "foreignKey" to be a string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_ONE,
+              model: value,
+              foreignKey: 'modelId',
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "hasOne", ' +
+                'so it requires the option "model" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('model')();
+        });
+
+        it('throws an error if the option "discriminator" is provided', function () {
+          const throwable = () =>
+            S.validate('model', {
+              foo: {
+                type: RelationType.HAS_ONE,
+                model: 'model',
+                foreignKey: 'modelId',
+                discriminator: 'modelType',
+              },
+            });
+          expect(throwable).to.throw(
+            'The relation "foo" of the model "model" is a non-polymorphic "hasOne" relation, ' +
+              'so it should not have the option "discriminator" to be provided.',
+          );
+        });
+      });
+
+      describe('a polymorphic relation with a target relation name', function () {
+        it('requires the option "model" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_ONE,
+              model: value,
+              polymorphic: 'reference',
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "hasOne", ' +
+                'so it requires the option "model" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('model')();
+        });
+
+        it('throws an error if the option "foreignKey" is provided', function () {
+          const throwable = () =>
+            S.validate('model', {
+              foo: {
+                type: RelationType.HAS_ONE,
+                model: 'model',
+                polymorphic: 'reference',
+                foreignKey: 'referenceId',
+              },
+            });
+          expect(throwable).to.throw(
+            'The relation "foo" of the model "model" has the option "polymorphic" with ' +
+              'a String value, so it should not have the option "foreignKey" ' +
+              'to be provided.',
+          );
+        });
+
+        it('throws an error if the option "discriminator" is provided', function () {
+          const throwable = () =>
+            S.validate('model', {
+              foo: {
+                type: RelationType.HAS_ONE,
+                model: 'model',
+                polymorphic: 'reference',
+                discriminator: 'referenceType',
+              },
+            });
+          expect(throwable).to.throw(
+            'The relation "foo" of the model "model" has the option "polymorphic" with ' +
+              'a String value, so it should not have the option "discriminator" ' +
+              'to be provided.',
+          );
+        });
+      });
+
+      describe('a polymorphic relation with target relation keys', function () {
+        it('requires the option "model" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_ONE,
+              model: value,
+              polymorphic: true,
+              foreignKey: 'referenceId',
+              discriminator: 'referenceType',
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "hasOne", ' +
+                'so it requires the option "model" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('model')();
+        });
+
+        it('requires the option "foreignKey" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_ONE,
+              model: 'model',
+              polymorphic: true,
+              foreignKey: value,
+              discriminator: 'referenceType',
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the option "polymorphic" ' +
+                'with "true" value, so it requires the option "foreignKey" ' +
+                'to be a non-empty String, but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('referenceId')();
+        });
+
+        it('requires the option "discriminator" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_ONE,
+              model: 'model',
+              polymorphic: true,
+              foreignKey: 'referenceId',
+              discriminator: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the option "polymorphic" ' +
+                'with "true" value, so it requires the option "discriminator" ' +
+                'to be a non-empty String, but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('referenceType')();
+        });
+      });
+    });
+
+    describe('hasMany', function () {
+      describe('a regular relation', function () {
+        it('requires the option "model" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_MANY,
+              model: 'model',
+              foreignKey: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "hasMany", ' +
+                'so it requires the option "foreignKey" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('modelId')();
+        });
+
+        it('requires the option "foreignKey" to be a string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_MANY,
+              model: value,
+              foreignKey: 'modelId',
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "hasMany", ' +
+                'so it requires the option "model" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('model')();
+        });
+
+        it('throws an error if the option "discriminator" is provided', function () {
+          const throwable = () =>
+            S.validate('model', {
+              foo: {
+                type: RelationType.HAS_MANY,
+                model: 'model',
+                foreignKey: 'modelId',
+                discriminator: 'modelType',
+              },
+            });
+          expect(throwable).to.throw(
+            'The relation "foo" of the model "model" is a non-polymorphic "hasMany" ' +
+              'relation, so it should not have the option "discriminator" to be provided.',
+          );
+        });
+      });
+
+      describe('a polymorphic relation with a target relation name', function () {
+        it('requires the option "model" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_MANY,
+              model: value,
+              polymorphic: 'reference',
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "hasMany", ' +
+                'so it requires the option "model" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('model')();
+        });
+
+        it('throws an error if the option "foreignKey" is provided', function () {
+          const throwable = () =>
+            S.validate('model', {
+              foo: {
+                type: RelationType.HAS_MANY,
+                model: 'model',
+                polymorphic: 'reference',
+                foreignKey: 'referenceId',
+              },
+            });
+          expect(throwable).to.throw(
+            'The relation "foo" of the model "model" has the option "polymorphic" ' +
+              'with a String value, so it should not have the option "foreignKey" ' +
+              'to be provided.',
+          );
+        });
+
+        it('throws an error if the option "discriminator" is provided', function () {
+          const throwable = () =>
+            S.validate('model', {
+              foo: {
+                type: RelationType.HAS_MANY,
+                model: 'model',
+                polymorphic: 'reference',
+                discriminator: 'referenceType',
+              },
+            });
+          expect(throwable).to.throw(
+            'The relation "foo" of the model "model" has the option "polymorphic" with ' +
+              'a String value, so it should not have the option "discriminator" ' +
+              'to be provided.',
+          );
+        });
+      });
+
+      describe('a polymorphic relation with target relation keys', function () {
+        it('requires the option "model" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_MANY,
+              model: value,
+              polymorphic: true,
+              foreignKey: 'referenceId',
+              discriminator: 'referenceType',
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the type "hasMany", ' +
+                'so it requires the option "model" to be a non-empty String, ' +
+                'but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('model')();
+        });
+
+        it('requires the option "foreignKey" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_MANY,
+              model: 'model',
+              polymorphic: true,
+              foreignKey: value,
+              discriminator: 'referenceType',
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the option "polymorphic" ' +
+                'with "true" value, so it requires the option "foreignKey" ' +
+                'to be a non-empty String, but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('referenceId')();
+        });
+
+        it('requires the option "discriminator" to be a non-empty string', function () {
+          const validate = value => {
+            const foo = {
+              type: RelationType.HAS_MANY,
+              model: 'model',
+              polymorphic: true,
+              foreignKey: 'referenceId',
+              discriminator: value,
+            };
+            return () => S.validate('model', {foo});
+          };
+          const createError = value =>
+            format(
+              'The relation "foo" of the model "model" has the option "polymorphic" ' +
+                'with "true" value, so it requires the option "discriminator" ' +
+                'to be a non-empty String, but %s given.',
+              value,
+            );
+          expect(validate('')).to.throw(createError('""'));
+          expect(validate(10)).to.throw(createError('10'));
+          expect(validate(true)).to.throw(createError('true'));
+          expect(validate(false)).to.throw(createError('false'));
+          expect(validate({})).to.throw(createError('Object'));
+          expect(validate([])).to.throw(createError('Array'));
+          expect(validate(undefined)).to.throw(createError('undefined'));
+          expect(validate(null)).to.throw(createError('null'));
+          validate('referenceType')();
+        });
+      });
+    });
+
+    describe('referencesMany', function () {
+      it('requires the option "model" to be a non-empty string', function () {
+        const validate = value => {
+          const foo = {
+            type: RelationType.REFERENCES_MANY,
+            model: value,
+          };
+          return () => S.validate('model', {foo});
+        };
+        const createError = value =>
+          format(
+            'The relation "foo" of the model "model" has the type "referencesMany", ' +
+              'so it requires the option "model" to be a non-empty String, ' +
+              'but %s given.',
+            value,
+          );
+        expect(validate('')).to.throw(createError('""'));
+        expect(validate(10)).to.throw(createError('10'));
+        expect(validate(true)).to.throw(createError('true'));
+        expect(validate(false)).to.throw(createError('false'));
+        expect(validate({})).to.throw(createError('Object'));
+        expect(validate([])).to.throw(createError('Array'));
+        expect(validate(undefined)).to.throw(createError('undefined'));
+        expect(validate(null)).to.throw(createError('null'));
+        validate('model')();
+      });
+
+      it('expects the provided option "foreignKey" to be a string', function () {
+        const validate = value => {
+          const foo = {
+            type: RelationType.REFERENCES_MANY,
+            model: 'model',
+            foreignKey: value,
+          };
+          return () => S.validate('model', {foo});
+        };
+        const createError = value =>
+          format(
+            'The relation "foo" of the model "model" has the type "referencesMany", ' +
+              'so it expects the provided option "foreignKey" to be a String, ' +
+              'but %s given.',
+            value,
+          );
+        expect(validate(10)).to.throw(createError('10'));
+        expect(validate(true)).to.throw(createError('true'));
+        expect(validate({})).to.throw(createError('Object'));
+        expect(validate([])).to.throw(createError('Array'));
+        validate('foreignKey')();
+        validate('')();
+        validate(false)();
+        validate(undefined)();
+        validate(null)();
+      });
+
+      it('throws an error if the option "discriminator" is provided', function () {
+        const throwable = () =>
+          S.validate('model', {
+            foo: {
+              type: RelationType.REFERENCES_MANY,
+              model: 'model',
+              discriminator: 'referenceType',
+            },
+          });
+        expect(throwable).to.throw(
+          'The relation "foo" of the model "model" has the type "referencesMany", ' +
+            'so it should not have the option "discriminator" to be provided.',
+        );
+      });
+    });
+  });
+});

+ 3 - 0
src/errors/index.js

@@ -0,0 +1,3 @@
+export * from './not-implemented-error.js';
+export * from './invalid-argument-error.js';
+export * from './invalid-operator-value-error.js';

+ 20 - 0
src/errors/invalid-argument-error.js

@@ -0,0 +1,20 @@
+import {format} from 'util';
+import {valueToString} from '../utils/index.js';
+
+/**
+ * Invalid argument error.
+ */
+export class InvalidArgumentError extends Error {
+  /**
+   * Constructor.
+   *
+   * @param pattern
+   * @param args
+   */
+  constructor(pattern, ...args) {
+    const vars = args.map(valueToString);
+    const message =
+      typeof pattern === 'string' ? format(pattern, ...vars) : undefined;
+    super(message);
+  }
+}

+ 33 - 0
src/errors/invalid-argument-error.spec.js

@@ -0,0 +1,33 @@
+import {expect} from 'chai';
+import {InvalidArgumentError} from './invalid-argument-error.js';
+
+describe('InvalidArgumentError', function () {
+  it('inherits from Error class', function () {
+    const error = new InvalidArgumentError();
+    expect(error).to.be.instanceof(Error);
+  });
+
+  it('sets a given message', function () {
+    const error = new InvalidArgumentError('This is the Error');
+    expect(error.message).to.be.eq('This is the Error');
+  });
+
+  it('interpolates a given pattern with variables', function () {
+    const error = new InvalidArgumentError(
+      '%s, %s, %s, %s, %s, %s, %s, %s, %s and %s',
+      'str',
+      10,
+      true,
+      false,
+      {},
+      [],
+      undefined,
+      null,
+      () => undefined,
+      function () {},
+    );
+    expect(error.message).to.be.eq(
+      '"str", 10, true, false, Object, Array, undefined, null, Function and Function',
+    );
+  });
+});

+ 25 - 0
src/errors/invalid-operator-value-error.js

@@ -0,0 +1,25 @@
+import {format} from 'util';
+import {valueToString} from '../utils/index.js';
+
+/**
+ * Invalid operator value error.
+ */
+export class InvalidOperatorValueError extends Error {
+  /**
+   * Constructor.
+   *
+   * @param operator
+   * @param expected
+   * @param value
+   */
+  constructor(operator, expected, value) {
+    super(
+      format(
+        'Condition of {%s: ...} should have %s, %s given.',
+        operator,
+        expected,
+        valueToString(value),
+      ),
+    );
+  }
+}

+ 11 - 0
src/errors/invalid-operator-value-error.spec.js

@@ -0,0 +1,11 @@
+import {expect} from 'chai';
+import {InvalidOperatorValueError} from './invalid-operator-value-error.js';
+
+describe('InvalidOperatorValueError', function () {
+  it('sets specific message', function () {
+    const error = new InvalidOperatorValueError('exists', 'a boolean', '');
+    const message =
+      'Condition of {exists: ...} should have a boolean, "" given.';
+    expect(error.message).to.be.eq(message);
+  });
+});

+ 20 - 0
src/errors/not-implemented-error.js

@@ -0,0 +1,20 @@
+import {format} from 'util';
+import {valueToString} from '../utils/index.js';
+
+/**
+ * Not implemented error.
+ */
+export class NotImplementedError extends Error {
+  /**
+   * Constructor.
+   *
+   * @param pattern
+   * @param args
+   */
+  constructor(pattern, ...args) {
+    const vars = args.map(valueToString);
+    const message =
+      typeof pattern === 'string' ? format(pattern, ...vars) : undefined;
+    super(message);
+  }
+}

+ 33 - 0
src/errors/not-implemented-error.spec.js

@@ -0,0 +1,33 @@
+import {expect} from 'chai';
+import {NotImplementedError} from './not-implemented-error.js';
+
+describe('NotImplementedError', function () {
+  it('inherits from Error class', function () {
+    const error = new NotImplementedError();
+    expect(error).to.be.instanceof(Error);
+  });
+
+  it('sets a given message', function () {
+    const error = new NotImplementedError('This is the Error');
+    expect(error.message).to.be.eq('This is the Error');
+  });
+
+  it('interpolates a given pattern with variables', function () {
+    const error = new NotImplementedError(
+      '%s, %s, %s, %s, %s, %s, %s, %s, %s and %s',
+      'str',
+      10,
+      true,
+      false,
+      {},
+      [],
+      undefined,
+      null,
+      () => undefined,
+      function () {},
+    );
+    expect(error.message).to.be.eq(
+      '"str", 10, true, false, Object, Array, undefined, null, Function and Function',
+    );
+  });
+});

+ 84 - 0
src/filter/fields-clause-tool.js

@@ -0,0 +1,84 @@
+import {Service} from '../service/index.js';
+import {selectObjectKeys} from '../utils/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {ModelDefinitionUtils} from '../definition/index.js';
+
+/**
+ * Field clause tool.
+ */
+export class FieldsClauseTool extends Service {
+  /**
+   * Filter.
+   *
+   * @param {object|object[]} entities
+   * @param {string} modelName
+   * @param {string|string[]} clause
+   * @return {object|object[]}
+   */
+  filter(entities, modelName, clause) {
+    const isArray = Array.isArray(entities);
+    entities = isArray ? entities : [entities];
+    entities.forEach(entity => {
+      if (!entity || typeof entity !== 'object' || Array.isArray(entity))
+        throw new InvalidArgumentError(
+          'A first argument of FieldClauseTool.filter should be an Object or ' +
+            'an Array of Object, but %s given.',
+          entity,
+        );
+    });
+
+    const fields = Array.isArray(clause) ? clause.slice() : [clause];
+    fields.forEach(field => {
+      if (!field || typeof field !== 'string')
+        throw new InvalidArgumentError(
+          'The provided option "fields" should be a String ' +
+            'or an Array of String, but %s given.',
+          field,
+        );
+    });
+
+    const pkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(modelName);
+    if (fields.indexOf(pkPropName) === -1) fields.push(pkPropName);
+
+    entities = entities.map(entity => selectObjectKeys(entity, fields));
+    return isArray ? entities : entities[0];
+  }
+
+  /**
+   * Validate fields clause.
+   *
+   * @param clause
+   */
+  static validateFieldsClause(clause) {
+    if (!clause) return;
+    const tempClause = Array.isArray(clause) ? clause : [clause];
+    tempClause.forEach(key => {
+      if (!key || typeof key !== 'string')
+        throw new InvalidArgumentError(
+          'The provided option "fields" should be a non-empty String ' +
+            'or an Array of String, but %s given.',
+          key,
+        );
+    });
+  }
+
+  /**
+   * Normalize fields clause.
+   *
+   * @param clause
+   */
+  static normalizeFieldsClause(clause) {
+    if (!clause) return;
+    clause = Array.isArray(clause) ? clause : [clause];
+    clause.forEach(key => {
+      if (!key || typeof key !== 'string')
+        throw new InvalidArgumentError(
+          'The provided option "fields" should be a non-empty String ' +
+            'or an Array of String, but %s given.',
+          key,
+        );
+    });
+    return clause;
+  }
+}

+ 134 - 0
src/filter/fields-clause-tool.spec.js

@@ -0,0 +1,134 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {Schema} from '../schema.js';
+import {FieldsClauseTool} from './fields-clause-tool.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../definition/index.js';
+
+const S = new Schema();
+const MODEL_NAME = 'model';
+S.defineModel({name: MODEL_NAME});
+const T = S.get(FieldsClauseTool);
+
+describe('FieldsClauseTool', function () {
+  describe('filter', function () {
+    it('returns an object with selected fields', function () {
+      const value = {foo: 'fooVal', bar: 'barVal', baz: 'bazVal'};
+      const fields = ['bar', 'baz'];
+      const result = T.filter(value, MODEL_NAME, fields);
+      expect(result).to.be.eql({bar: 'barVal', baz: 'bazVal'});
+    });
+
+    it('returns an array with selected fields', function () {
+      const value = [
+        {foo: 'fooVal', bar: 'barVal', baz: 'bazVal'},
+        {foo: 'fooVal', bar: 'barVal', baz: 'bazVal'},
+        {foo: 'fooVal', bar: 'barVal', baz: 'bazVal'},
+      ];
+      const fields = ['bar', 'baz'];
+      const result = T.filter(value, MODEL_NAME, fields);
+      expect(result[0]).to.be.eql({bar: 'barVal', baz: 'bazVal'});
+      expect(result[1]).to.be.eql({bar: 'barVal', baz: 'bazVal'});
+      expect(result[2]).to.be.eql({bar: 'barVal', baz: 'bazVal'});
+    });
+
+    it('includes a primary key', function () {
+      const value = {[DEF_PK]: 10, foo: 'fooVal', bar: 'barVal', baz: 'bazVal'};
+      const fields = ['bar', 'baz'];
+      const result = T.filter(value, MODEL_NAME, fields);
+      expect(result).to.be.eql({[DEF_PK]: 10, bar: 'barVal', baz: 'bazVal'});
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => T.filter(10, MODEL_NAME, ['bar']);
+      expect(throwable).to.throw(
+        'A first argument of FieldClauseTool.filter should be an Object or ' +
+          'an Array of Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if elements of a first argument is not an object', function () {
+      const throwable = () => T.filter([10], MODEL_NAME, ['bar']);
+      expect(throwable).to.throw(
+        'A first argument of FieldClauseTool.filter should be an Object or ' +
+          'an Array of Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if a second argument is not a string', function () {
+      const throwable = () => T.filter({}, MODEL_NAME, 10);
+      expect(throwable).to.throw(
+        'The provided option "fields" should be a String ' +
+          'or an Array of String, but 10 given.',
+      );
+    });
+
+    it('throws an error if elements of a second argument is not a string', function () {
+      const throwable = () => T.filter({}, MODEL_NAME, [10]);
+      expect(throwable).to.throw(
+        'The provided option "fields" should be a String ' +
+          'or an Array of String, but 10 given.',
+      );
+    });
+  });
+
+  describe('validateFieldsClause', function () {
+    it('requires a non-empty string or an array of non-empty strings', function () {
+      const validate = clause => () =>
+        FieldsClauseTool.validateFieldsClause(clause);
+      const error = value =>
+        format(
+          'The provided option "fields" should be a non-empty String ' +
+            'or an Array of String, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate([''])).to.throw(error('""'));
+      expect(validate([10])).to.throw(error('10'));
+      expect(validate([true])).to.throw(error('true'));
+      expect(validate([false])).to.throw(error('false'));
+      expect(validate([undefined])).to.throw(error('undefined'));
+      expect(validate([null])).to.throw(error('null'));
+      validate('');
+      validate(false);
+      validate(undefined);
+      validate(null);
+      validate('foo');
+      validate(['foo']);
+    });
+  });
+
+  describe('normalizeFieldsClause', function () {
+    it('returns an array of strings', function () {
+      const fn = FieldsClauseTool.normalizeFieldsClause;
+      expect(fn('foo')).to.be.eql(['foo']);
+      expect(fn(['foo'])).to.be.eql(['foo']);
+    });
+
+    it('requires a non-empty string or an array of non-empty strings', function () {
+      const fn = clause => () => FieldsClauseTool.normalizeFieldsClause(clause);
+      const error = value =>
+        format(
+          'The provided option "fields" should be a non-empty String ' +
+            'or an Array of String, but %s given.',
+          value,
+        );
+      expect(fn(10)).to.throw(error('10'));
+      expect(fn(true)).to.throw(error('true'));
+      expect(fn({})).to.throw(error('Object'));
+      expect(fn([''])).to.throw(error('""'));
+      expect(fn([10])).to.throw(error('10'));
+      expect(fn([true])).to.throw(error('true'));
+      expect(fn([false])).to.throw(error('false'));
+      expect(fn([undefined])).to.throw(error('undefined'));
+      expect(fn([null])).to.throw(error('null'));
+      expect(fn('')()).to.be.undefined;
+      expect(fn(false)()).to.be.undefined;
+      expect(fn(undefined)()).to.be.undefined;
+      expect(fn(null)()).to.be.undefined;
+      expect(fn('foo')()).to.be.eql(['foo']);
+      expect(fn(['foo'])()).to.be.eql(['foo']);
+    });
+  });
+});

+ 362 - 0
src/filter/include-clause-tool.js

@@ -0,0 +1,362 @@
+import {Service} from '../service/index.js';
+import {RelationType} from '../definition/index.js';
+import {HasOneResolver} from '../relations/index.js';
+import {HasManyResolver} from '../relations/index.js';
+import {WhereClauseTool} from './where-clause-tool.js';
+import {OrderClauseTool} from './order-clause-tool.js';
+import {SliceClauseTool} from './slice-clause-tool.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {BelongsToResolver} from '../relations/index.js';
+import {FieldsClauseTool} from './fields-clause-tool.js';
+import {ModelDefinitionUtils} from '../definition/index.js';
+import {ReferencesManyResolver} from '../relations/index.js';
+
+/**
+ * Include clause tool.
+ */
+export class IncludeClauseTool extends Service {
+  /**
+   * Include to.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} modelName
+   * @param {Record<string, unknown>[]} clause
+   * @return {Promise<void>}
+   */
+  async includeTo(entities, modelName, clause) {
+    clause = IncludeClauseTool.normalizeIncludeClause(clause);
+    const promises = [];
+    clause.forEach(inclusion => {
+      const relDef = this.get(ModelDefinitionUtils).getRelationDefinitionByName(
+        modelName,
+        inclusion.relation,
+      );
+      switch (relDef.type) {
+        // BELONGS_TO
+        case RelationType.BELONGS_TO:
+          if (relDef.polymorphic) {
+            promises.push(
+              this.get(BelongsToResolver).includePolymorphicTo(
+                entities,
+                modelName,
+                inclusion.relation,
+                relDef.foreignKey,
+                relDef.discriminator,
+                inclusion.scope,
+              ),
+            );
+          } else {
+            promises.push(
+              this.get(BelongsToResolver).includeTo(
+                entities,
+                modelName,
+                relDef.model,
+                inclusion.relation,
+                relDef.foreignKey,
+                inclusion.scope,
+              ),
+            );
+          }
+          break;
+        // HAS_ONE
+        case RelationType.HAS_ONE:
+          if (relDef.polymorphic && typeof relDef.polymorphic === 'string') {
+            promises.push(
+              this.get(HasOneResolver).includePolymorphicByRelationName(
+                entities,
+                modelName,
+                relDef.model,
+                inclusion.relation,
+                relDef.polymorphic,
+                inclusion.scope,
+              ),
+            );
+          } else if (relDef.polymorphic) {
+            promises.push(
+              this.get(HasOneResolver).includePolymorphicTo(
+                entities,
+                modelName,
+                relDef.model,
+                inclusion.relation,
+                relDef.foreignKey,
+                relDef.discriminator,
+                inclusion.scope,
+              ),
+            );
+          } else {
+            promises.push(
+              this.get(HasOneResolver).includeTo(
+                entities,
+                modelName,
+                relDef.model,
+                inclusion.relation,
+                relDef.foreignKey,
+                inclusion.scope,
+              ),
+            );
+          }
+          break;
+        // HAS_MANY
+        case RelationType.HAS_MANY:
+          if (relDef.polymorphic && typeof relDef.polymorphic === 'string') {
+            promises.push(
+              this.get(HasManyResolver).includePolymorphicByRelationName(
+                entities,
+                modelName,
+                relDef.model,
+                inclusion.relation,
+                relDef.polymorphic,
+                inclusion.scope,
+              ),
+            );
+          } else if (relDef.polymorphic) {
+            promises.push(
+              this.get(HasManyResolver).includePolymorphicTo(
+                entities,
+                modelName,
+                relDef.model,
+                inclusion.relation,
+                relDef.foreignKey,
+                relDef.discriminator,
+                inclusion.scope,
+              ),
+            );
+          } else {
+            promises.push(
+              this.get(HasManyResolver).includeTo(
+                entities,
+                modelName,
+                relDef.model,
+                inclusion.relation,
+                relDef.foreignKey,
+                inclusion.scope,
+              ),
+            );
+          }
+          break;
+        case RelationType.REFERENCES_MANY:
+          promises.push(
+            this.get(ReferencesManyResolver).includeTo(
+              entities,
+              modelName,
+              relDef.model,
+              inclusion.relation,
+              relDef.foreignKey,
+              inclusion.scope,
+            ),
+          );
+          break;
+        default:
+          throw new InvalidArgumentError(
+            'The relation type %s does not have an inclusion resolver.',
+            relDef.type,
+          );
+      }
+    });
+    await Promise.all(promises);
+  }
+
+  /**
+   * Validate include clause.
+   *
+   * @param clause
+   */
+  static validateIncludeClause(clause) {
+    if (!clause) {
+      // empty
+    } else if (typeof clause === 'string') {
+      // string
+    } else if (Array.isArray(clause)) {
+      // array
+      const relNames = [];
+      clause.flat().forEach(el => {
+        this.validateIncludeClause(el);
+        if (typeof el === 'string') {
+          relNames.push(el);
+        } else if (typeof el === 'object') {
+          Object.keys(el).forEach(key => {
+            if (Object.prototype.hasOwnProperty.call(el, key))
+              relNames.push(key);
+          });
+        }
+      });
+      // duplicates checking
+      const duplicateNames = relNames.filter(
+        (name, i) => relNames.indexOf(name) !== i,
+      );
+      if (duplicateNames.length)
+        throw new InvalidArgumentError(
+          'The provided option "include" has duplicates of %s.',
+          duplicateNames[0],
+        );
+    } else if (typeof clause === 'object') {
+      // object
+      if ('relation' in clause) {
+        // {relation: 'name', scope: {}}
+        if (!clause.relation || typeof clause.relation !== 'string')
+          throw new InvalidArgumentError(
+            'The provided option "relation" should be ' +
+              'a non-empty String, but %s given.',
+            clause.relation,
+          );
+        if ('scope' in clause && clause) this.validateScopeClause(clause.scope);
+      } else {
+        // {foo: 'bar', 'baz': ['qux'], ...}
+        Object.keys(clause).forEach(key => {
+          if (!Object.prototype.hasOwnProperty.call(clause, key)) return;
+          this.validateIncludeClause(clause[key]);
+        });
+      }
+    } else {
+      // unknown.
+      throw new InvalidArgumentError(
+        'The provided option "include" should have a value of ' +
+          'following types: String, Object or Array, but %s given.',
+        clause,
+      );
+    }
+  }
+
+  /**
+   * Validate scope clause.
+   *
+   * @param clause
+   */
+  static validateScopeClause(clause) {
+    if (!clause) return;
+    if (typeof clause !== 'object' || Array.isArray(clause))
+      throw new InvalidArgumentError(
+        'The provided option "scope" should be an Object, but %s given.',
+        clause,
+      );
+    if ('where' in clause && clause.where)
+      WhereClauseTool.validateWhereClause(clause.where);
+    if ('order' in clause && clause.order)
+      OrderClauseTool.validateOrderClause(clause.order);
+    if ('skip' in clause && clause.skip)
+      SliceClauseTool.validateSkipClause(clause.skip);
+    if ('limit' in clause && clause.limit)
+      SliceClauseTool.validateLimitClause(clause.limit);
+    if ('fields' in clause && clause.fields)
+      FieldsClauseTool.validateFieldsClause(clause.fields);
+    if ('include' in clause && clause.include)
+      IncludeClauseTool.validateIncludeClause(clause.include);
+  }
+
+  /**
+   * Normalize include clause.
+   *
+   * @param clause
+   */
+  static normalizeIncludeClause(clause) {
+    let result = [];
+    if (!clause) {
+      // empty
+      return result;
+    } else if (typeof clause === 'string') {
+      // string
+      result.push({relation: clause});
+    } else if (Array.isArray(clause)) {
+      // array
+      clause.flat().forEach(el => {
+        if (Array.isArray(el)) {
+          el = el
+            .flat()
+            .map(v => this.normalizeIncludeClause(v))
+            .flat();
+        } else {
+          el = this.normalizeIncludeClause(el);
+        }
+        result = [...result, ...el];
+      });
+      // duplicates checking
+      const relNames = result.map(v => v.relation);
+      const duplicateNames = relNames.filter(
+        (name, i) => relNames.indexOf(name) !== i,
+      );
+      if (duplicateNames.length)
+        throw new InvalidArgumentError(
+          'The provided option "include" has duplicates of %s.',
+          duplicateNames[0],
+        );
+    } else if (typeof clause === 'object') {
+      // object
+      if ('relation' in clause) {
+        // {relation: 'name', scope: {...}}
+        if (!clause.relation || typeof clause.relation !== 'string')
+          throw new InvalidArgumentError(
+            'The provided option "relation" should be ' +
+              'a non-empty String, but %s given.',
+            clause.relation,
+          );
+        const normalized = {relation: clause.relation};
+        const scope = this.normalizeScopeClause(clause.scope);
+        if (scope) normalized.scope = scope;
+        result.push(normalized);
+      } else {
+        // {foo: 'bar', baz: ['qux'], ...}
+        Object.keys(clause).forEach(key => {
+          if (!Object.prototype.hasOwnProperty.call(clause, key)) return;
+          const normalized = {relation: key};
+          const include = this.normalizeIncludeClause(clause[key]);
+          if (include.length) normalized.scope = {include};
+          result.push(normalized);
+        });
+      }
+    } else {
+      // unknown
+      throw new InvalidArgumentError(
+        'The provided option "include" should have a value of ' +
+          'following types: String, Object or Array, but %s given.',
+        clause,
+      );
+    }
+    return result;
+  }
+
+  /**
+   * Normalize scope clause.
+   *
+   * @param clause
+   * @return {undefined|{}}
+   */
+  static normalizeScopeClause(clause) {
+    if (!clause) return;
+    if (typeof clause !== 'object' || Array.isArray(clause))
+      throw new InvalidArgumentError(
+        'The provided option "scope" should be an Object, but %s given.',
+        clause,
+      );
+    const result = {};
+    // {where: ...}
+    if ('where' in clause && clause.where) {
+      WhereClauseTool.validateWhereClause(clause.where);
+      result.where = clause.where;
+    }
+    // {order: ...}
+    if ('order' in clause && clause.order) {
+      OrderClauseTool.validateOrderClause(clause.order);
+      result.order = clause.order;
+    }
+    // {skip: ...}
+    if ('skip' in clause && clause.skip) {
+      SliceClauseTool.validateSkipClause(clause.skip);
+      result.skip = clause.skip;
+    }
+    // {limit: ...}
+    if ('limit' in clause && clause.limit) {
+      SliceClauseTool.validateLimitClause(clause.limit);
+      result.limit = clause.limit;
+    }
+    // {fields: ...}
+    if ('fields' in clause && clause.fields) {
+      FieldsClauseTool.validateFieldsClause(clause.fields);
+      result.fields = clause.fields;
+    }
+    // {include: ...}
+    if ('include' in clause && clause.include)
+      result.include = this.normalizeIncludeClause(clause.include);
+    if (Object.keys(result).length) return result;
+    return undefined;
+  }
+}

+ 653 - 0
src/filter/include-clause-tool.spec.js

@@ -0,0 +1,653 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {IncludeClauseTool} from './include-clause-tool.js';
+
+describe('IncludeClauseTool', function () {
+  describe('validateIncludeClause', function () {
+    it('does not throw for valid values', function () {
+      const validate = v => IncludeClauseTool.validateIncludeClause(v);
+      // empty
+      validate(0);
+      validate('');
+      validate(null);
+      validate(undefined);
+      // strings
+      validate('foo');
+      // arrays
+      validate(['foo']);
+      validate([['foo']]);
+      validate([{foo: 'bar'}]);
+      validate([{foo: ['bar']}]);
+      validate([{foo: {bar: 'baz'}}]);
+      validate([{foo: [{bar: 'baz'}]}]);
+      validate([{relation: 'foo', scope: {where: {bar: 'baz'}}}]);
+      validate([{relation: 'foo', scope: {include: 'bar'}}]);
+      validate([{relation: 'foo', scope: {include: ['bar']}}]);
+      validate([{relation: 'foo', scope: {include: {bar: 'baz'}}}]);
+      validate([{relation: 'foo', scope: {include: [{bar: 'baz'}]}}]);
+      // objects
+      validate({foo: 'bar'});
+      validate({foo: ['bar']});
+      validate({foo: {bar: 'baz'}});
+      validate({foo: [{bar: 'baz'}]});
+      validate({relation: 'foo'});
+      validate({relation: 'foo', scope: {where: {bar: 'baz'}}});
+      validate({relation: 'foo', scope: {include: 'bar'}});
+      validate({relation: 'foo', scope: {include: ['bar']}});
+      validate({relation: 'foo', scope: {include: {bar: 'baz'}}});
+      validate({relation: 'foo', scope: {include: [{bar: 'baz'}]}});
+    });
+
+    it('throws an error for unsupported values', function () {
+      const validate = v => () => IncludeClauseTool.validateIncludeClause(v);
+      const createError = v =>
+        format(
+          'The provided option "include" should have a value of ' +
+            'following types: String, Object or Array, but %s given.',
+          v,
+        );
+      const testFor = (v, s) => {
+        const error = createError(s);
+        const clauses = [
+          v,
+          // arrays
+          [v],
+          [{foo: v}],
+          [{foo: [v]}],
+          [{foo: {bar: v}}],
+          [{foo: {bar: [v]}}],
+          [{foo: [{bar: v}]}],
+          [{foo: [{bar: [v]}]}],
+          [{relation: 'foo', scope: {include: v}}],
+          [{relation: 'foo', scope: {include: {bar: v}}}],
+          // objects
+          {foo: v},
+          {foo: [v]},
+          {foo: {bar: v}},
+          {foo: {bar: [v]}},
+          {foo: [{bar: v}]},
+          {foo: [{bar: [v]}]},
+          {relation: 'foo', scope: {include: v}},
+          {relation: 'foo', scope: {include: {bar: v}}},
+        ];
+        clauses.forEach(c => expect(validate(c)).to.throw(error));
+      };
+      testFor(10, '10');
+      testFor(true, 'true');
+      testFor(() => undefined, 'Function');
+    });
+
+    it('throws an error for duplicates', function () {
+      const validate = v => () => IncludeClauseTool.validateIncludeClause(v);
+      const error = 'The provided option "include" has duplicates of "foo".';
+      const clauses = [
+        ['foo', 'foo'],
+        [['foo'], 'foo'],
+        ['foo', ['foo']],
+        [['foo'], ['foo']],
+        ['foo', {foo: 'bar'}],
+        [{foo: 'bar'}, 'foo'],
+        [{foo: 'bar'}, {foo: 'bar'}],
+        [[{foo: 'bar'}], 'foo'],
+        ['foo', [{foo: 'bar'}]],
+        [[{foo: 'bar'}], [{foo: 'bar'}]],
+      ];
+      clauses.forEach(c => expect(validate(c)).to.throw(error));
+      validate({foo: 'foo'})();
+      validate([{foo: 'foo'}])();
+    });
+  });
+
+  describe('normalizeClause', function () {
+    it('normalizes a given string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause('test');
+      expect(result).to.be.eql([{relation: 'test'}]);
+    });
+
+    it('normalizes a free-form object', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({foo: 'bar'});
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes a free-form object with a nested array', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({foo: ['bar']});
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes a free-form object with a nested free-form object', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {bar: 'baz'},
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes a free-form object with a nested inclusion object', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {baz: 'qux'},
+            include: 'baz',
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {baz: 'qux'},
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes an inclusion object', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: 'bar',
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes an inclusion object with a nested array', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: ['bar'],
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes an inclusion object with a nested free-form object', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: {bar: 'baz'},
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes an inclusion object with a nested inclusion object', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: [
+            {
+              relation: 'bar',
+              scope: {
+                where: {removed: false},
+                order: 'myId',
+                skip: 10,
+                limit: 5,
+                fields: ['id', 'removed'],
+                include: 'qwe',
+              },
+            },
+          ],
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'myId',
+                  skip: 10,
+                  limit: 5,
+                  fields: ['id', 'removed'],
+                  include: [
+                    {
+                      relation: 'qwe',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause(['foo', 'bar']);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+        },
+        {
+          relation: 'bar',
+        },
+      ]);
+    });
+
+    it('normalizes an array of nested free-form objects', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {foo: 'bar'},
+        {baz: 'qux'},
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'baz',
+          scope: {
+            include: [
+              {
+                relation: 'qux',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes an array of nested inclusion objects', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: 'bar',
+          },
+        },
+        {
+          relation: 'baz',
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'baz',
+        },
+      ]);
+    });
+
+    it('normalizes an array of nested arrays', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        ['foo'],
+        ['bar', ['baz']],
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+        },
+        {
+          relation: 'bar',
+        },
+        {
+          relation: 'baz',
+        },
+      ]);
+    });
+
+    it('normalizes a free-form object with mixed inclusions', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        // a string
+        foo: 'bar',
+        // an array
+        baz: ['qux'],
+        // a free-form object
+        bat: {qwe: 'asd'},
+        // an inclusion object
+        zxc: {
+          relation: 'rty',
+          scope: {
+            fields: ['id', 'featured'],
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        // a string
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+        // an array
+        {
+          relation: 'baz',
+          scope: {
+            include: [
+              {
+                relation: 'qux',
+              },
+            ],
+          },
+        },
+        // a free-form object
+        {
+          relation: 'bat',
+          scope: {
+            include: [
+              {
+                relation: 'qwe',
+                scope: {
+                  include: [
+                    {
+                      relation: 'asd',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        // an inclusion object
+        {
+          relation: 'zxc',
+          scope: {
+            include: [
+              {
+                relation: 'rty',
+                scope: {
+                  fields: ['id', 'featured'],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes an array with mixed inclusions', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        // a string
+        'foo',
+        // a free-form object
+        {
+          bar: 'baz',
+          qux: {
+            relation: 'qwe',
+          },
+        },
+        // an inclusion object
+        {
+          relation: 'asd',
+          scope: {
+            include: 'zxc',
+          },
+        },
+        // a nested array
+        ['rty', 'fgh', ['vbn']],
+      ]);
+      expect(result).to.be.eql([
+        // a string
+        {
+          relation: 'foo',
+        },
+        // a free-form object
+        {
+          relation: 'bar',
+          scope: {
+            include: [
+              {
+                relation: 'baz',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qux',
+          scope: {
+            include: [
+              {
+                relation: 'qwe',
+              },
+            ],
+          },
+        },
+        // an inclusion object
+        {
+          relation: 'asd',
+          scope: {
+            include: [
+              {
+                relation: 'zxc',
+              },
+            ],
+          },
+        },
+        // a nested array
+        {
+          relation: 'rty',
+        },
+        {
+          relation: 'fgh',
+        },
+        {
+          relation: 'vbn',
+        },
+      ]);
+    });
+
+    it('throws an error for unsupported values', function () {
+      const validate = v => () => IncludeClauseTool.normalizeIncludeClause(v);
+      const createError = v =>
+        format(
+          'The provided option "include" should have a value of ' +
+            'following types: String, Object or Array, but %s given.',
+          v,
+        );
+      const testFor = (v, s) => {
+        const error = createError(s);
+        const clauses = [
+          v,
+          // arrays
+          [v],
+          [{foo: v}],
+          [{foo: [v]}],
+          [{foo: {bar: v}}],
+          [{foo: {bar: [v]}}],
+          [{foo: [{bar: v}]}],
+          [{foo: [{bar: [v]}]}],
+          [{relation: 'foo', scope: {include: v}}],
+          [{relation: 'foo', scope: {include: {bar: v}}}],
+          // objects
+          {foo: v},
+          {foo: [v]},
+          {foo: {bar: v}},
+          {foo: {bar: [v]}},
+          {foo: [{bar: v}]},
+          {foo: [{bar: [v]}]},
+          {relation: 'foo', scope: {include: v}},
+          {relation: 'foo', scope: {include: {bar: v}}},
+        ];
+        clauses.forEach(c => expect(validate(c)).to.throw(error));
+      };
+      testFor(10, '10');
+      testFor(true, 'true');
+      testFor(() => undefined, 'Function');
+    });
+
+    it('throws an error for duplicates', function () {
+      const validate = v => () => IncludeClauseTool.normalizeIncludeClause(v);
+      const error = 'The provided option "include" has duplicates of "foo".';
+      const clauses = [
+        ['foo', 'foo'],
+        [['foo'], 'foo'],
+        ['foo', ['foo']],
+        [['foo'], ['foo']],
+        ['foo', {foo: 'bar'}],
+        [{foo: 'bar'}, 'foo'],
+        [{foo: 'bar'}, {foo: 'bar'}],
+        [[{foo: 'bar'}], 'foo'],
+        ['foo', [{foo: 'bar'}]],
+        [[{foo: 'bar'}], [{foo: 'bar'}]],
+      ];
+      clauses.forEach(c => expect(validate(c)).to.throw(error));
+      validate({foo: 'foo'})();
+      validate([{foo: 'foo'}])();
+    });
+  });
+});

+ 6 - 0
src/filter/index.js

@@ -0,0 +1,6 @@
+export * from './slice-clause-tool.js';
+export * from './order-clause-tool.js';
+export * from './where-clause-tool.js';
+export * from './fields-clause-tool.js';
+export * from './include-clause-tool.js';
+export * from './operator-clause-tool.js';

+ 503 - 0
src/filter/operator-clause-tool.js

@@ -0,0 +1,503 @@
+import {Service} from '../service/index.js';
+import {stringToRegexp} from '../utils/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {InvalidOperatorValueError} from '../errors/index.js';
+
+/**
+ * Operator clause tool.
+ */
+export class OperatorClauseTool extends Service {
+  /**
+   * Compare.
+   *
+   * @param {*} val1 The 1st value
+   * @param {*} val2 The 2nd value
+   * @returns {number} 0: =, positive: >, negative <
+   */
+  compare(val1, val2) {
+    if (val1 == null || val2 == null) {
+      return val1 == val2 ? 0 : NaN;
+    }
+    if (typeof val1 === 'number') {
+      if (
+        typeof val2 === 'number' ||
+        typeof val2 === 'string' ||
+        typeof val2 === 'boolean'
+      ) {
+        if (val1 === val2) return 0;
+        return val1 - Number(val2);
+      }
+      return NaN;
+    }
+    if (typeof val1 === 'string') {
+      const isDigits = /^\d+$/.test(val1);
+      if (isDigits) return this.compare(Number(val1), val2);
+      try {
+        if (val1 > val2) return 1;
+        if (val1 < val2) return -1;
+        if (val1 == val2) return 0;
+      } catch (e) {
+        /**/
+      }
+      return NaN;
+    }
+    if (typeof val1 === 'boolean') {
+      return Number(val1) - Number(val2);
+    }
+    // Return NaN if we don't know how to compare.
+    return val1 === val2 ? 0 : NaN;
+  }
+
+  /**
+   * Test all operators.
+   *
+   * @param clause
+   * @param value
+   */
+  testAll(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testAll ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+
+    // {eq: ...}
+    // {neq: ...}
+    const eqNeqTest = this.testEqNeq(clause, value);
+    if (eqNeqTest !== undefined) return eqNeqTest;
+
+    // {gt: ...}
+    // {gte: ...}
+    // {lt: ...}
+    // {lte: ...}
+    const gtLtTest = this.testGtLt(clause, value);
+    if (gtLtTest !== undefined) return gtLtTest;
+
+    // {inc: ...}
+    const incTest = this.testInq(clause, value);
+    if (incTest !== undefined) return incTest;
+
+    // {nin: ...}
+    const ninTest = this.testNin(clause, value);
+    if (ninTest !== undefined) return ninTest;
+
+    // {between: ...}
+    const betweenTest = this.testBetween(clause, value);
+    if (betweenTest !== undefined) return betweenTest;
+
+    // {exists: ...}
+    const existsTest = this.testExists(clause, value);
+    if (existsTest !== undefined) return existsTest;
+
+    // {like: ...}
+    const likeTest = this.testLike(clause, value);
+    if (likeTest !== undefined) return likeTest;
+
+    // {nlike: ...}
+    const nlikeTest = this.testNlike(clause, value);
+    if (nlikeTest !== undefined) return nlikeTest;
+
+    // {ilike: ...}
+    const ilikeTest = this.testIlike(clause, value);
+    if (ilikeTest !== undefined) return ilikeTest;
+
+    // {nilike: ...}
+    const nilikeTest = this.testNilike(clause, value);
+    if (nilikeTest !== undefined) return nilikeTest;
+
+    // {regexp: ...}
+    const regExpTest = this.testRegexp(clause, value);
+    if (regExpTest !== undefined) return regExpTest;
+  }
+
+  /**
+   * Test eq/neq operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   eq: 'foo',
+   * }
+   * ```
+   *
+   * @example
+   * ```ts
+   * {
+   *   neq: 'foo',
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testEqNeq(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testEqNeq ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('eq' in clause) return this.compare(clause.eq, value) === 0;
+    if ('neq' in clause) return this.compare(clause.neq, value) !== 0;
+  }
+
+  /**
+   * Test lt/gt/lte/gte operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   lt: 10,
+   * }
+   * ```
+   *
+   * @example
+   * ```ts
+   * {
+   *   lte: 10,
+   * }
+   * ```
+   *
+   * @example
+   * ```ts
+   * {
+   *   gt: 10,
+   * }
+   * ```
+   *
+   * @example
+   * ```ts
+   * {
+   *   gte: 10,
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testGtLt(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testGtLt ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('gt' in clause) return this.compare(value, clause.gt) > 0;
+    if ('gte' in clause) return this.compare(value, clause.gte) >= 0;
+    if ('lt' in clause) return this.compare(value, clause.lt) < 0;
+    if ('lte' in clause) return this.compare(value, clause.lte) <= 0;
+  }
+
+  /**
+   * Test inc operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   inc: ['foo', 'bar'],
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testInq(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testInq ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('inq' in clause && clause.inq !== undefined) {
+      if (!clause.inq || !Array.isArray(clause.inq)) {
+        throw new InvalidOperatorValueError(
+          'inq',
+          'an Array of possible values',
+          clause.inq,
+        );
+      }
+      for (let i = 0; i < clause.inq.length; i++) {
+        if (clause.inq[i] == value) return true;
+      }
+      return false;
+    }
+  }
+
+  /**
+   * Test nin operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   nin: ['foo', 'bar'],
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testNin(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testNin ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('nin' in clause && clause.nin !== undefined) {
+      if (!clause.nin || !Array.isArray(clause.nin)) {
+        throw new InvalidOperatorValueError(
+          'nin',
+          'an Array of possible values',
+          clause.nin,
+        );
+      }
+      for (let i = 0; i < clause.nin.length; i++) {
+        if (clause.nin[i] == value) return false;
+      }
+      return true;
+    }
+  }
+
+  /**
+   * Test between operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   between: [10, 20],
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testBetween(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testBetween ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('between' in clause && clause.between !== undefined) {
+      if (!Array.isArray(clause.between) || clause.between.length !== 2) {
+        throw new InvalidOperatorValueError(
+          'between',
+          'an Array of 2 elements',
+          clause.between,
+        );
+      }
+      return (
+        this.testGtLt({gte: clause.between[0]}, value) &&
+        this.testGtLt({lte: clause.between[1]}, value)
+      );
+    }
+  }
+
+  /**
+   * Test exists operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   exists: true,
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testExists(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testExists ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('exists' in clause && clause.exists !== undefined) {
+      if (typeof clause.exists !== 'boolean') {
+        throw new InvalidOperatorValueError(
+          'exists',
+          'a Boolean',
+          clause.exists,
+        );
+      }
+      return clause.exists ? value !== undefined : value === undefined;
+    }
+  }
+
+  /**
+   * Test like operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   like: 'foo',
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testLike(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testLike ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('like' in clause && clause.like !== undefined) {
+      if (typeof clause.like !== 'string' && !(clause.like instanceof RegExp))
+        throw new InvalidOperatorValueError('like', 'a String', clause.like);
+      return stringToRegexp(clause.like).test(value);
+    }
+  }
+
+  /**
+   * Test nlike operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   nlike: 'foo',
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testNlike(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testNlike ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('nlike' in clause && clause.nlike !== undefined) {
+      if (
+        typeof clause.nlike !== 'string' &&
+        !(clause.nlike instanceof RegExp)
+      ) {
+        throw new InvalidOperatorValueError('nlike', 'a String', clause.nlike);
+      }
+      return !stringToRegexp(clause.nlike).test(value);
+    }
+  }
+
+  /**
+   * Test ilike operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   ilike: 'foo',
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testIlike(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testIlike ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('ilike' in clause && clause.ilike !== undefined) {
+      if (
+        typeof clause.ilike !== 'string' &&
+        !(clause.ilike instanceof RegExp)
+      ) {
+        throw new InvalidOperatorValueError('ilike', 'a String', clause.ilike);
+      }
+      return stringToRegexp(clause.ilike, 'i').test(value);
+    }
+  }
+
+  /**
+   * Test nilike operator.
+   *
+   * @example
+   * ```ts
+   * {
+   *   nilike: 'foo',
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testNilike(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testNilike ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('nilike' in clause && clause.nilike !== undefined) {
+      if (
+        typeof clause.nilike !== 'string' &&
+        !(clause.nilike instanceof RegExp)
+      ) {
+        throw new InvalidOperatorValueError(
+          'nilike',
+          'a String',
+          clause.nilike,
+        );
+      }
+      return !stringToRegexp(clause.nilike, 'i').test(value);
+    }
+  }
+
+  /**
+   * Test regexp.
+   *
+   * @example
+   * ```ts
+   * {
+   *   regexp: 'foo.*',
+   * }
+   * ```
+   *
+   * @example
+   * ```ts
+   * {
+   *   regexp: 'foo.*',
+   *   flags: 'i',
+   * }
+   * ```
+   *
+   * @param clause
+   * @param value
+   */
+  testRegexp(clause, value) {
+    if (!clause || typeof clause !== 'object')
+      throw new InvalidArgumentError(
+        'A first argument of OperatorUtils.testRegexp ' +
+          'should be an Object, but %s given.',
+        clause,
+      );
+    if ('regexp' in clause && clause.regexp !== undefined) {
+      if (
+        typeof clause.regexp !== 'string' &&
+        !(clause.regexp instanceof RegExp)
+      ) {
+        throw new InvalidOperatorValueError(
+          'regexp',
+          'a String',
+          clause.regexp,
+        );
+      }
+      const flags = clause.flags || undefined;
+      if (flags && typeof flags !== 'string')
+        throw new InvalidArgumentError(
+          'RegExp flags must be a String, but %s given.',
+          clause.flags,
+        );
+      if (!value || typeof value !== 'string') return false;
+      const regExp = stringToRegexp(clause.regexp, flags);
+      return !!value.match(regExp);
+    }
+  }
+}

+ 1064 - 0
src/filter/operator-clause-tool.spec.js

@@ -0,0 +1,1064 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {OperatorClauseTool} from './operator-clause-tool.js';
+import {InvalidOperatorValueError} from '../errors/index.js';
+
+const S = new OperatorClauseTool();
+
+describe('OperatorClauseTool', function () {
+  describe('compare', function () {
+    it('returns a negative number if a second value is greatest', function () {
+      expect(S.compare(0, 5)).to.be.eq(-5);
+      expect(S.compare(0, '5')).to.be.eq(-5);
+      expect(S.compare(0, true)).to.be.eq(-1);
+      expect(S.compare('0', 5)).to.be.eq(-5);
+      expect(S.compare('a', 'b')).to.be.eq(-1);
+    });
+
+    it('returns a positive number if a second value is lowest', function () {
+      expect(S.compare(5, 0)).to.be.eq(5);
+      expect(S.compare(5, '0')).to.be.eq(5);
+      expect(S.compare(5, false)).to.be.eq(5);
+      expect(S.compare(5, true)).to.be.eq(4);
+      expect(S.compare('5', 0)).to.be.eq(5);
+      expect(S.compare('b', 'a')).to.be.eq(1);
+    });
+
+    it('returns zero if given values are equal', function () {
+      const obj = {};
+      expect(S.compare(0, 0)).to.be.eq(0);
+      expect(S.compare(0, '0')).to.be.eq(0);
+      expect(S.compare('0', 0)).to.be.eq(0);
+      expect(S.compare('a', 'a')).to.be.eq(0);
+      expect(S.compare(obj, obj)).to.be.eq(0);
+      expect(S.compare(null, null)).to.be.eq(0);
+      expect(S.compare(undefined, undefined)).to.be.eq(0);
+    });
+
+    it('returns NaN if we do not know how to compare', function () {
+      expect(isNaN(S.compare(null, 'string'))).to.be.true;
+      expect(isNaN(S.compare(null, 10))).to.be.true;
+      expect(isNaN(S.compare([], 0))).to.be.true;
+      expect(isNaN(S.compare([], []))).to.be.true;
+      expect(isNaN(S.compare({}, {}))).to.be.true;
+      expect(isNaN(S.compare(10, {}))).to.be.true;
+      expect(isNaN(S.compare('string', Symbol()))).to.be.true;
+    });
+  });
+
+  describe('testAll', function () {
+    it('tests "eq" and "neq" operators', function () {
+      expect(S.testAll({eq: 10}, 10)).to.be.true;
+      expect(S.testAll({eq: 10}, 9)).to.be.false;
+      expect(S.testAll({neq: 10}, 9)).to.be.true;
+      expect(S.testAll({neq: 10}, 10)).to.be.false;
+    });
+
+    it('tests "gt", "gte", "lt" and "lte" operators', function () {
+      expect(S.testAll({gt: 5}, 6)).to.be.true;
+      expect(S.testAll({gte: 5}, 5)).to.be.true;
+      expect(S.testAll({lt: 5}, 4)).to.be.true;
+      expect(S.testAll({lte: 5}, 5)).to.be.true;
+      expect(S.testAll({gt: 5}, 5)).to.be.false;
+      expect(S.testAll({gte: 5}, 4)).to.be.false;
+      expect(S.testAll({lt: 5}, 5)).to.be.false;
+      expect(S.testAll({lte: 5}, 6)).to.be.false;
+    });
+
+    it('tests a "inq" operator', function () {
+      expect(S.testAll({inq: [1, 2, 3]}, 2)).to.be.true;
+      expect(S.testAll({inq: [1, 2, 3]}, 'a')).to.be.false;
+    });
+
+    it('tests a "nin" operator', function () {
+      expect(S.testAll({nin: [1, 2, 3]}, 'a')).to.be.true;
+      expect(S.testAll({nin: [1, 2, 3]}, 2)).to.be.false;
+    });
+
+    it('tests a "between" operator', function () {
+      expect(S.testAll({between: [-2, 2]}, 0)).to.be.true;
+      expect(S.testAll({between: [-2, 2]}, 10)).to.be.false;
+    });
+
+    it('tests an "exists" operator', function () {
+      expect(S.testAll({exists: true}, 10)).to.be.true;
+      expect(S.testAll({exists: false}, undefined)).to.be.true;
+      expect(S.testAll({exists: true}, undefined)).to.be.false;
+      expect(S.testAll({exists: false}, 10)).to.be.false;
+    });
+
+    it('tests a "like" operator', function () {
+      expect(S.testAll({like: 'World'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({like: 'world'}, 'Hello World!')).to.be.false;
+    });
+
+    it('tests a "nlike" operator', function () {
+      expect(S.testAll({nlike: 'John'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({nlike: 'World'}, 'Hello World!')).to.be.false;
+    });
+
+    it('tests a "ilike" operator', function () {
+      expect(S.testAll({ilike: 'WORLD'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({ilike: 'John'}, 'Hello World!')).to.be.false;
+    });
+
+    it('tests a "nilike" operator', function () {
+      expect(S.testAll({nilike: 'John'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({nilike: 'world'}, 'Hello World!')).to.be.false;
+    });
+
+    it('tests a "regexp" operator', function () {
+      expect(S.testAll({regexp: 'Wo.+'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({regexp: 'Fo.+'}, 'Hello World!')).to.be.false;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testAll(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testAll ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+  });
+
+  describe('testEqNeq', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testEqNeq({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testEqNeq(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testEqNeq ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    describe('eq', function () {
+      it('returns true if a given value is equal to reference', function () {
+        expect(S.testEqNeq({eq: 0}, 0)).to.be.true;
+        expect(S.testEqNeq({eq: 0}, '0')).to.be.true;
+        expect(S.testEqNeq({eq: 0}, false)).to.be.true;
+        expect(S.testEqNeq({eq: 1}, true)).to.be.true;
+        expect(S.testEqNeq({eq: 'a'}, 'a')).to.be.true;
+        expect(S.testEqNeq({eq: true}, true)).to.be.true;
+        expect(S.testEqNeq({eq: false}, false)).to.be.true;
+        expect(S.testEqNeq({eq: Infinity}, Infinity)).to.be.true;
+        expect(S.testEqNeq({eq: null}, null)).to.be.true;
+        expect(S.testEqNeq({eq: undefined}, undefined)).to.be.true;
+      });
+
+      it('returns false if a given value is not-equal to reference', function () {
+        expect(S.testEqNeq({eq: 0}, 1)).to.be.false;
+        expect(S.testEqNeq({eq: 0}, '1')).to.be.false;
+        expect(S.testEqNeq({eq: 0}, true)).to.be.false;
+        expect(S.testEqNeq({eq: 0}, Infinity)).to.be.false;
+        expect(S.testEqNeq({eq: 0}, null)).to.be.false;
+        expect(S.testEqNeq({eq: 0}, undefined)).to.be.false;
+        expect(S.testEqNeq({eq: '0'}, '1')).to.be.false;
+        expect(S.testEqNeq({eq: '0'}, true)).to.be.false;
+        expect(S.testEqNeq({eq: '0'}, Infinity)).to.be.false;
+        expect(S.testEqNeq({eq: '0'}, null)).to.be.false;
+        expect(S.testEqNeq({eq: '0'}, undefined)).to.be.false;
+        expect(S.testEqNeq({eq: true}, false)).to.be.false;
+        expect(S.testEqNeq({eq: true}, null)).to.be.false;
+        expect(S.testEqNeq({eq: true}, undefined)).to.be.false;
+        expect(S.testEqNeq({eq: false}, true)).to.be.false;
+        expect(S.testEqNeq({eq: false}, null)).to.be.false;
+        expect(S.testEqNeq({eq: false}, undefined)).to.be.false;
+      });
+    });
+
+    describe('neq', function () {
+      it('returns false if a given value is strictly equal to reference', function () {
+        expect(S.testEqNeq({neq: 0}, 0)).to.be.false;
+        expect(S.testEqNeq({neq: 0}, '0')).to.be.false;
+        expect(S.testEqNeq({neq: 0}, false)).to.be.false;
+        expect(S.testEqNeq({neq: 1}, true)).to.be.false;
+        expect(S.testEqNeq({neq: 'a'}, 'a')).to.be.false;
+        expect(S.testEqNeq({neq: true}, true)).to.be.false;
+        expect(S.testEqNeq({neq: false}, false)).to.be.false;
+        expect(S.testEqNeq({neq: Infinity}, Infinity)).to.be.false;
+        expect(S.testEqNeq({neq: null}, null)).to.be.false;
+        expect(S.testEqNeq({neq: undefined}, undefined)).to.be.false;
+      });
+
+      it('returns true if a given value is strictly not-equal to reference', function () {
+        expect(S.testEqNeq({neq: 0}, 1)).to.be.true;
+        expect(S.testEqNeq({neq: 0}, '1')).to.be.true;
+        expect(S.testEqNeq({neq: 0}, true)).to.be.true;
+        expect(S.testEqNeq({neq: 0}, Infinity)).to.be.true;
+        expect(S.testEqNeq({neq: 0}, null)).to.be.true;
+        expect(S.testEqNeq({neq: 0}, undefined)).to.be.true;
+        expect(S.testEqNeq({neq: '0'}, '1')).to.be.true;
+        expect(S.testEqNeq({neq: '0'}, true)).to.be.true;
+        expect(S.testEqNeq({neq: '0'}, Infinity)).to.be.true;
+        expect(S.testEqNeq({neq: '0'}, null)).to.be.true;
+        expect(S.testEqNeq({neq: '0'}, undefined)).to.be.true;
+        expect(S.testEqNeq({neq: true}, false)).to.be.true;
+        expect(S.testEqNeq({neq: true}, null)).to.be.true;
+        expect(S.testEqNeq({neq: true}, undefined)).to.be.true;
+        expect(S.testEqNeq({neq: false}, true)).to.be.true;
+        expect(S.testEqNeq({neq: false}, null)).to.be.true;
+        expect(S.testEqNeq({neq: false}, undefined)).to.be.true;
+      });
+    });
+  });
+
+  describe('testGtLt', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testGtLt({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testGtLt(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testGtLt ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    describe('gt', function () {
+      it('returns true if a given value is greater than reference', function () {
+        expect(S.testGtLt({gt: 0}, 5)).to.be.true;
+        expect(S.testGtLt({gt: 0}, '5')).to.be.true;
+        expect(S.testGtLt({gt: 0}, Infinity)).to.be.true;
+        expect(S.testGtLt({gt: 0}, true)).to.be.true;
+        expect(S.testGtLt({gt: -1}, false)).to.be.true;
+        expect(S.testGtLt({gt: 'a'}, 'b')).to.be.true;
+      });
+
+      it('returns false if a given value is equal to reference', function () {
+        expect(S.testGtLt({gt: 0}, 0)).to.be.false;
+        expect(S.testGtLt({gt: 0}, '0')).to.be.false;
+        expect(S.testGtLt({gt: 0}, false)).to.be.false;
+        expect(S.testGtLt({gt: 1}, true)).to.be.false;
+        expect(S.testGtLt({gt: 'a'}, 'a')).to.be.false;
+      });
+
+      it('returns false if a given value is lower than reference', function () {
+        expect(S.testGtLt({gt: 2}, 0)).to.be.false;
+        expect(S.testGtLt({gt: 2}, '0')).to.be.false;
+        expect(S.testGtLt({gt: 2}, true)).to.be.false;
+        expect(S.testGtLt({gt: 2}, false)).to.be.false;
+        expect(S.testGtLt({gt: 'b'}, 'a')).to.be.false;
+      });
+
+      it('returns false if we do not know how to compare', function () {
+        expect(S.testGtLt({gt: 1}, [])).to.be.false;
+        expect(S.testGtLt({gt: 1}, {})).to.be.false;
+        expect(S.testGtLt({gt: 1}, null)).to.be.false;
+        expect(S.testGtLt({gt: 1}, undefined)).to.be.false;
+        expect(S.testGtLt({gt: 1}, NaN)).to.be.false;
+        expect(S.testGtLt({gt: 1}, Symbol())).to.be.false;
+      });
+    });
+
+    describe('gte', function () {
+      it('returns true if a given value is greater than reference', function () {
+        expect(S.testGtLt({gte: 0}, 5)).to.be.true;
+        expect(S.testGtLt({gte: 0}, '5')).to.be.true;
+        expect(S.testGtLt({gte: 0}, Infinity)).to.be.true;
+        expect(S.testGtLt({gte: 0}, true)).to.be.true;
+        expect(S.testGtLt({gte: -1}, false)).to.be.true;
+        expect(S.testGtLt({gte: 'a'}, 'b')).to.be.true;
+      });
+
+      it('returns true if a given value is equal to reference', function () {
+        expect(S.testGtLt({gte: 0}, 0)).to.be.true;
+        expect(S.testGtLt({gte: 0}, '0')).to.be.true;
+        expect(S.testGtLt({gte: 0}, false)).to.be.true;
+        expect(S.testGtLt({gte: 1}, true)).to.be.true;
+        expect(S.testGtLt({gte: 'a'}, 'a')).to.be.true;
+      });
+
+      it('returns false if a given value is lower than reference', function () {
+        expect(S.testGtLt({gte: 2}, 0)).to.be.false;
+        expect(S.testGtLt({gte: 2}, '0')).to.be.false;
+        expect(S.testGtLt({gte: 2}, true)).to.be.false;
+        expect(S.testGtLt({gte: 2}, false)).to.be.false;
+        expect(S.testGtLt({gte: 'b'}, 'a')).to.be.false;
+      });
+
+      it('returns false if we do not know how to compare', function () {
+        expect(S.testGtLt({gte: 1}, [])).to.be.false;
+        expect(S.testGtLt({gte: 1}, {})).to.be.false;
+        expect(S.testGtLt({gte: 1}, null)).to.be.false;
+        expect(S.testGtLt({gte: 1}, undefined)).to.be.false;
+        expect(S.testGtLt({gte: 1}, NaN)).to.be.false;
+        expect(S.testGtLt({gte: 1}, Symbol())).to.be.false;
+      });
+    });
+
+    describe('lt', function () {
+      it('returns false if a given value is greater than reference', function () {
+        expect(S.testGtLt({lt: 0}, 5)).to.be.false;
+        expect(S.testGtLt({lt: 0}, '5')).to.be.false;
+        expect(S.testGtLt({lt: 0}, Infinity)).to.be.false;
+        expect(S.testGtLt({lt: 0}, true)).to.be.false;
+        expect(S.testGtLt({lt: -1}, false)).to.be.false;
+        expect(S.testGtLt({lt: 'a'}, 'b')).to.be.false;
+      });
+
+      it('returns false if a given value is equal to reference', function () {
+        expect(S.testGtLt({lt: 0}, 0)).to.be.false;
+        expect(S.testGtLt({lt: 0}, '0')).to.be.false;
+        expect(S.testGtLt({lt: 0}, false)).to.be.false;
+        expect(S.testGtLt({lt: 1}, true)).to.be.false;
+        expect(S.testGtLt({lt: 'a'}, 'a')).to.be.false;
+      });
+
+      it('returns true if a given value is lower than reference', function () {
+        expect(S.testGtLt({lt: 2}, 0)).to.be.true;
+        expect(S.testGtLt({lt: 2}, '0')).to.be.true;
+        expect(S.testGtLt({lt: 2}, true)).to.be.true;
+        expect(S.testGtLt({lt: 2}, false)).to.be.true;
+        expect(S.testGtLt({lt: 'b'}, 'a')).to.be.true;
+      });
+
+      it('returns false if we do not know how to compare', function () {
+        expect(S.testGtLt({lt: 1}, [])).to.be.false;
+        expect(S.testGtLt({lt: 1}, {})).to.be.false;
+        expect(S.testGtLt({lt: 1}, null)).to.be.false;
+        expect(S.testGtLt({lt: 1}, undefined)).to.be.false;
+        expect(S.testGtLt({lt: 1}, NaN)).to.be.false;
+        expect(S.testGtLt({lt: 1}, Symbol())).to.be.false;
+      });
+    });
+
+    describe('lte', function () {
+      it('returns false if a given value is greater than reference', function () {
+        expect(S.testGtLt({lte: 0}, 5)).to.be.false;
+        expect(S.testGtLt({lte: 0}, '5')).to.be.false;
+        expect(S.testGtLt({lte: 0}, Infinity)).to.be.false;
+        expect(S.testGtLt({lte: 0}, true)).to.be.false;
+        expect(S.testGtLt({lte: -1}, false)).to.be.false;
+        expect(S.testGtLt({lte: 'a'}, 'b')).to.be.false;
+      });
+
+      it('returns true if a given value is equal to reference', function () {
+        expect(S.testGtLt({lte: 0}, 0)).to.be.true;
+        expect(S.testGtLt({lte: 0}, '0')).to.be.true;
+        expect(S.testGtLt({lte: 0}, false)).to.be.true;
+        expect(S.testGtLt({lte: 1}, true)).to.be.true;
+        expect(S.testGtLt({lte: 'a'}, 'a')).to.be.true;
+      });
+
+      it('returns true if a given value is lower than reference', function () {
+        expect(S.testGtLt({lte: 2}, 0)).to.be.true;
+        expect(S.testGtLt({lte: 2}, '0')).to.be.true;
+        expect(S.testGtLt({lte: 2}, true)).to.be.true;
+        expect(S.testGtLt({lte: 2}, false)).to.be.true;
+        expect(S.testGtLt({lte: 'b'}, 'a')).to.be.true;
+      });
+
+      it('returns false if we do not know how to compare', function () {
+        expect(S.testGtLt({lte: 1}, [])).to.be.false;
+        expect(S.testGtLt({lte: 1}, {})).to.be.false;
+        expect(S.testGtLt({lte: 1}, null)).to.be.false;
+        expect(S.testGtLt({lte: 1}, undefined)).to.be.false;
+        expect(S.testGtLt({lte: 1}, NaN)).to.be.false;
+        expect(S.testGtLt({lte: 1}, Symbol())).to.be.false;
+      });
+    });
+  });
+
+  describe('testInq', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testInq({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns true if a given value has in array', function () {
+      expect(S.testInq({inq: [1, 2]}, 2)).to.be.true;
+      expect(S.testInq({inq: [1, 2]}, '2')).to.be.true;
+      expect(S.testInq({inq: ['a', 'b']}, 'b')).to.be.true;
+      expect(S.testInq({inq: [1, 2]}, true)).to.be.true;
+      expect(S.testInq({inq: [-1, 0]}, false)).to.be.true;
+    });
+
+    it('returns false if a given value is not in array', function () {
+      expect(S.testInq({inq: [1, 2]}, 3)).to.be.false;
+      expect(S.testInq({inq: [1, 2]}, '3')).to.be.false;
+      expect(S.testInq({inq: ['a', 'b']}, 'c')).to.be.false;
+      expect(S.testInq({inq: [-1, 0]}, true)).to.be.false;
+      expect(S.testInq({inq: [1, 2]}, false)).to.be.false;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testInq(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testInq ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const inq = 10;
+      const throwable = () => S.testInq({inq}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a string', function () {
+      const inq = '10';
+      const throwable = () => S.testInq({inq}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const inq = {};
+      const throwable = () => S.testInq({inq}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const inq = null;
+      const throwable = () => S.testInq({inq}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+  });
+
+  describe('testNin', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testNin({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns false if a given value has in array', function () {
+      expect(S.testNin({nin: [1, 2]}, 2)).to.be.false;
+      expect(S.testNin({nin: [1, 2]}, '2')).to.be.false;
+      expect(S.testNin({nin: ['a', 'b']}, 'b')).to.be.false;
+      expect(S.testNin({nin: [1, 2]}, true)).to.be.false;
+      expect(S.testNin({nin: [-1, 0]}, false)).to.be.false;
+    });
+
+    it('returns true if a given value is not in array', function () {
+      expect(S.testNin({nin: [1, 2]}, 3)).to.be.true;
+      expect(S.testNin({nin: [1, 2]}, '3')).to.be.true;
+      expect(S.testNin({nin: ['a', 'b']}, 'c')).to.be.true;
+      expect(S.testNin({nin: [-1, 0]}, true)).to.be.true;
+      expect(S.testNin({nin: [1, 2]}, false)).to.be.true;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testNin(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testNin ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const nin = 10;
+      const throwable = () => S.testNin({nin}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a string', function () {
+      const nin = '10';
+      const throwable = () => S.testNin({nin}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const nin = {};
+      const throwable = () => S.testNin({nin}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const nin = null;
+      const throwable = () => S.testNin({nin}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+  });
+
+  describe('testBetween', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testBetween({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns true if a given value exists in a range', function () {
+      expect(S.testBetween({between: [-2, 2]}, -2)).to.be.true;
+      expect(S.testBetween({between: [-2, 2]}, 0)).to.be.true;
+      expect(S.testBetween({between: [-2, 2]}, 2)).to.be.true;
+      expect(S.testBetween({between: [-2, 2]}, '-2')).to.be.true;
+      expect(S.testBetween({between: [-2, 2]}, '0')).to.be.true;
+      expect(S.testBetween({between: [-2, 2]}, '2')).to.be.true;
+      expect(S.testBetween({between: ['b', 'd']}, 'b')).to.be.true;
+      expect(S.testBetween({between: ['b', 'd']}, 'c')).to.be.true;
+      expect(S.testBetween({between: ['b', 'd']}, 'd')).to.be.true;
+      expect(S.testBetween({between: [-2, 2]}, true)).to.be.true;
+      expect(S.testBetween({between: [-2, 2]}, false)).to.be.true;
+    });
+
+    it('returns false if a given value not exists in a range', function () {
+      expect(S.testBetween({between: [-2, 2]}, -5)).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, 5)).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, '-5')).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, '5')).to.be.false;
+      expect(S.testBetween({between: ['b', 'd']}, 'a')).to.be.false;
+      expect(S.testBetween({between: ['b', 'd']}, 'e')).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, null)).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, undefined)).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, NaN)).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, Infinity)).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, Symbol())).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, [])).to.be.false;
+      expect(S.testBetween({between: [-2, 2]}, {})).to.be.false;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testBetween(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testBetween ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if a given range is not valid', function () {
+      const setOf1 = [10];
+      const setOf2 = [10, 20];
+      const setOf3 = [10, 20, 30];
+      const throwable1 = () => S.testBetween({between: setOf1}, 10);
+      const throwable2 = () => S.testBetween({between: setOf2}, 10);
+      const throwable3 = () => S.testBetween({between: setOf3}, 10);
+      expect(throwable1).to.throw(InvalidOperatorValueError);
+      expect(throwable2).to.not.throw(InvalidOperatorValueError);
+      expect(throwable3).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const between = 10;
+      const throwable = () => S.testBetween({between}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a string', function () {
+      const between = '10';
+      const throwable = () => S.testBetween({between}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a boolean', function () {
+      const between = true;
+      const throwable = () => S.testBetween({between}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const between = {};
+      const throwable = () => S.testBetween({between}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const between = null;
+      const throwable = () => S.testBetween({between}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+  });
+
+  describe('testExists', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testExists({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testExists(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testExists ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    describe('exists', function () {
+      it('returns true for non-undefined values', function () {
+        expect(S.testExists({exists: true}, 0)).to.be.true;
+        expect(S.testExists({exists: true}, '')).to.be.true;
+        expect(S.testExists({exists: true}, null)).to.be.true;
+        expect(S.testExists({exists: true}, NaN)).to.be.true;
+        expect(S.testExists({exists: true}, Infinity)).to.be.true;
+        expect(S.testExists({exists: true}, Symbol())).to.be.true;
+        expect(S.testExists({exists: true}, true)).to.be.true;
+        expect(S.testExists({exists: true}, false)).to.be.true;
+        expect(S.testExists({exists: true}, [])).to.be.true;
+        expect(S.testExists({exists: true}, {})).to.be.true;
+      });
+
+      it('returns false for undefined value', function () {
+        expect(S.testExists({exists: true}, undefined)).to.be.false;
+      });
+    });
+
+    describe('not exists', function () {
+      it('returns false for non-undefined values', function () {
+        expect(S.testExists({exists: false}, 0)).to.be.false;
+        expect(S.testExists({exists: false}, '')).to.be.false;
+        expect(S.testExists({exists: false}, null)).to.be.false;
+        expect(S.testExists({exists: false}, NaN)).to.be.false;
+        expect(S.testExists({exists: false}, Infinity)).to.be.false;
+        expect(S.testExists({exists: false}, Symbol())).to.be.false;
+        expect(S.testExists({exists: false}, true)).to.be.false;
+        expect(S.testExists({exists: false}, false)).to.be.false;
+        expect(S.testExists({exists: false}, [])).to.be.false;
+        expect(S.testExists({exists: false}, {})).to.be.false;
+      });
+
+      it('returns true for undefined value', function () {
+        expect(S.testExists({exists: false}, undefined)).to.be.true;
+      });
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const exists = 10;
+      const throwable = () => S.testExists({exists}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a string', function () {
+      const exists = '10';
+      const throwable = () => S.testExists({exists}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const exists = {};
+      const throwable = () => S.testExists({exists}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const exists = null;
+      const throwable = () => S.testExists({exists}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+  });
+
+  describe('testLike', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testLike({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns true if a given value matches a substring', function () {
+      expect(S.testLike({like: 'val'}, 'value')).to.be.true;
+      expect(S.testLike({like: 'lue'}, 'value')).to.be.true;
+      expect(S.testLike({like: 'value'}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value not matches a substring', function () {
+      expect(S.testLike({like: 'value'}, 'val')).to.be.false;
+      expect(S.testLike({like: 'value'}, 'lue')).to.be.false;
+      expect(S.testLike({like: 'value'}, 'foo')).to.be.false;
+    });
+
+    it('uses case-sensitive matching for a substring', function () {
+      expect(S.testLike({like: 'Val'}, 'value')).to.be.false;
+      expect(S.testLike({like: 'Val'}, 'Value')).to.be.true;
+      expect(S.testLike({like: 'val'}, 'Value')).to.be.false;
+    });
+
+    it('returns true if a given value matches a string expression', function () {
+      expect(S.testLike({like: 'val.+'}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value not matches a string expression', function () {
+      expect(S.testLike({like: 'foo.+'}, 'value')).to.be.false;
+    });
+
+    it('uses case-sensitive matching for a string expression', function () {
+      expect(S.testLike({like: 'Val.+'}, 'value')).to.be.false;
+      expect(S.testLike({like: 'Val.+'}, 'Value')).to.be.true;
+      expect(S.testLike({like: 'val.+'}, 'Value')).to.be.false;
+    });
+
+    it('returns true if a given value matches a RegExp', function () {
+      expect(S.testLike({like: new RegExp(/val.+/)}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value matches a RegExp', function () {
+      expect(S.testLike({like: new RegExp(/foo.+/)}, 'value')).to.be.false;
+    });
+
+    it('uses case-sensitive matching for a RegExp', function () {
+      expect(S.testLike({like: new RegExp(/Val.+/)}, 'value')).to.be.false;
+      expect(S.testLike({like: new RegExp(/Val.+/)}, 'Value')).to.be.true;
+      expect(S.testLike({like: new RegExp(/val.+/)}, 'Value')).to.be.false;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testLike(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testLike ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const like = 10;
+      const throwable = () => S.testLike({like}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const like = {};
+      const throwable = () => S.testLike({like}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const like = null;
+      const throwable = () => S.testLike({like}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+  });
+
+  describe('testNlike', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testNlike({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns false if a given value matches a substring', function () {
+      expect(S.testNlike({nlike: 'val'}, 'value')).to.be.false;
+      expect(S.testNlike({nlike: 'lue'}, 'value')).to.be.false;
+      expect(S.testNlike({nlike: 'value'}, 'value')).to.be.false;
+    });
+
+    it('returns true if a given value not matches a substring', function () {
+      expect(S.testNlike({nlike: 'value'}, 'val')).to.be.true;
+      expect(S.testNlike({nlike: 'value'}, 'lue')).to.be.true;
+      expect(S.testNlike({nlike: 'value'}, 'foo')).to.be.true;
+    });
+
+    it('uses case-sensitive matching for a substring', function () {
+      expect(S.testNlike({nlike: 'Val'}, 'value')).to.be.true;
+      expect(S.testNlike({nlike: 'Val'}, 'Value')).to.be.false;
+      expect(S.testNlike({nlike: 'val'}, 'Value')).to.be.true;
+    });
+
+    it('returns false if a given value matches a string expression', function () {
+      expect(S.testNlike({nlike: 'val.+'}, 'value')).to.be.false;
+    });
+
+    it('returns true if a given value not matches a string expression', function () {
+      expect(S.testNlike({nlike: 'foo.+'}, 'value')).to.be.true;
+    });
+
+    it('uses case-sensitive matching for a string expression', function () {
+      expect(S.testNlike({nlike: 'Val.+'}, 'value')).to.be.true;
+      expect(S.testNlike({nlike: 'Val.+'}, 'Value')).to.be.false;
+      expect(S.testNlike({nlike: 'val.+'}, 'Value')).to.be.true;
+    });
+
+    it('returns false if a given value matches a RegExp', function () {
+      expect(S.testNlike({nlike: new RegExp(/val.+/)}, 'value')).to.be.false;
+    });
+
+    it('returns true if a given value matches a RegExp', function () {
+      expect(S.testNlike({nlike: new RegExp(/foo.+/)}, 'value')).to.be.true;
+    });
+
+    it('uses case-sensitive matching for a RegExp', function () {
+      expect(S.testNlike({nlike: new RegExp(/Val.+/)}, 'value')).to.be.true;
+      expect(S.testNlike({nlike: new RegExp(/Val.+/)}, 'Value')).to.be.false;
+      expect(S.testNlike({nlike: new RegExp(/val.+/)}, 'Value')).to.be.true;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testNlike(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testNlike ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const nlike = 10;
+      const throwable = () => S.testNlike({nlike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const nlike = {};
+      const throwable = () => S.testNlike({nlike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const nlike = null;
+      const throwable = () => S.testNlike({nlike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+  });
+
+  describe('testIlike', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testIlike({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns true if a given value matches a substring', function () {
+      expect(S.testIlike({ilike: 'val'}, 'value')).to.be.true;
+      expect(S.testIlike({ilike: 'lue'}, 'value')).to.be.true;
+      expect(S.testIlike({ilike: 'value'}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value not matches a substring', function () {
+      expect(S.testIlike({ilike: 'value'}, 'val')).to.be.false;
+      expect(S.testIlike({ilike: 'value'}, 'lue')).to.be.false;
+      expect(S.testIlike({ilike: 'value'}, 'foo')).to.be.false;
+    });
+
+    it('uses case-insensitive matching for a substring', function () {
+      expect(S.testIlike({ilike: 'Val'}, 'value')).to.be.true;
+      expect(S.testIlike({ilike: 'Val'}, 'Value')).to.be.true;
+      expect(S.testIlike({ilike: 'val'}, 'Value')).to.be.true;
+    });
+
+    it('returns true if a given value matches a string expression', function () {
+      expect(S.testIlike({ilike: 'val.+'}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value not matches a string expression', function () {
+      expect(S.testIlike({ilike: 'foo.+'}, 'value')).to.be.false;
+    });
+
+    it('uses case-insensitive matching for a string expression', function () {
+      expect(S.testIlike({ilike: 'Val.+'}, 'value')).to.be.true;
+      expect(S.testIlike({ilike: 'Val.+'}, 'Value')).to.be.true;
+      expect(S.testIlike({ilike: 'val.+'}, 'Value')).to.be.true;
+    });
+
+    it('returns true if a given value matches a RegExp', function () {
+      expect(S.testIlike({ilike: new RegExp(/val.+/)}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value matches a RegExp', function () {
+      expect(S.testIlike({ilike: new RegExp(/foo.+/)}, 'value')).to.be.false;
+    });
+
+    it('uses case-insensitive matching for a RegExp', function () {
+      expect(S.testIlike({ilike: new RegExp(/Val.+/)}, 'value')).to.be.true;
+      expect(S.testIlike({ilike: new RegExp(/Val.+/)}, 'Value')).to.be.true;
+      expect(S.testIlike({ilike: new RegExp(/val.+/)}, 'Value')).to.be.true;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testIlike(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testIlike ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const ilike = 10;
+      const throwable = () => S.testIlike({ilike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const ilike = {};
+      const throwable = () => S.testIlike({ilike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const ilike = null;
+      const throwable = () => S.testIlike({ilike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+  });
+
+  describe('testNilike', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testNilike({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns false if a given value matches a substring', function () {
+      expect(S.testNilike({nilike: 'val'}, 'value')).to.be.false;
+      expect(S.testNilike({nilike: 'lue'}, 'value')).to.be.false;
+      expect(S.testNilike({nilike: 'value'}, 'value')).to.be.false;
+    });
+
+    it('returns true if a given value not matches a substring', function () {
+      expect(S.testNilike({nilike: 'value'}, 'val')).to.be.true;
+      expect(S.testNilike({nilike: 'value'}, 'lue')).to.be.true;
+      expect(S.testNilike({nilike: 'value'}, 'foo')).to.be.true;
+    });
+
+    it('uses case-insensitive matching for a substring', function () {
+      expect(S.testNilike({nilike: 'Val'}, 'value')).to.be.false;
+      expect(S.testNilike({nilike: 'Val'}, 'Value')).to.be.false;
+      expect(S.testNilike({nilike: 'val'}, 'Value')).to.be.false;
+    });
+
+    it('returns false if a given value matches a string expression', function () {
+      expect(S.testNilike({nilike: 'val.+'}, 'value')).to.be.false;
+    });
+
+    it('returns true if a given value not matches a string expression', function () {
+      expect(S.testNilike({nilike: 'foo.+'}, 'value')).to.be.true;
+    });
+
+    it('uses case-insensitive matching for a string expression', function () {
+      expect(S.testNilike({nilike: 'Val.+'}, 'value')).to.be.false;
+      expect(S.testNilike({nilike: 'Val.+'}, 'Value')).to.be.false;
+      expect(S.testNilike({nilike: 'val.+'}, 'Value')).to.be.false;
+    });
+
+    it('returns false if a given value matches a RegExp', function () {
+      expect(S.testNilike({nilike: new RegExp(/val.+/)}, 'value')).to.be.false;
+    });
+
+    it('returns true if a given value matches a RegExp', function () {
+      expect(S.testNilike({nilike: new RegExp(/foo.+/)}, 'value')).to.be.true;
+    });
+
+    it('uses case-insensitive matching for a RegExp', function () {
+      expect(S.testNilike({nilike: new RegExp(/Val.+/)}, 'value')).to.be.false;
+      expect(S.testNilike({nilike: new RegExp(/Val.+/)}, 'Value')).to.be.false;
+      expect(S.testNilike({nilike: new RegExp(/val.+/)}, 'Value')).to.be.false;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testNilike(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testNilike ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const nilike = 10;
+      const throwable = () => S.testNilike({nilike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const nilike = {};
+      const throwable = () => S.testNilike({nilike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const nilike = null;
+      const throwable = () => S.testNilike({nilike}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+  });
+
+  describe('testRegexp', function () {
+    it('returns undefined if no operator given', function () {
+      const result = S.testRegexp({}, 'value');
+      expect(result).to.be.undefined;
+    });
+
+    it('returns true if a given value matches a substring', function () {
+      expect(S.testRegexp({regexp: 'val'}, 'value')).to.be.true;
+      expect(S.testRegexp({regexp: 'lue'}, 'value')).to.be.true;
+      expect(S.testRegexp({regexp: 'value'}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value not matches a substring', function () {
+      expect(S.testRegexp({regexp: 'value'}, 'val')).to.be.false;
+      expect(S.testRegexp({regexp: 'value'}, 'lue')).to.be.false;
+      expect(S.testRegexp({regexp: 'value'}, 'foo')).to.be.false;
+    });
+
+    it('uses case-sensitive matching for a substring', function () {
+      expect(S.testRegexp({regexp: 'Val'}, 'value')).to.be.false;
+      expect(S.testRegexp({regexp: 'Val'}, 'Value')).to.be.true;
+      expect(S.testRegexp({regexp: 'val'}, 'Value')).to.be.false;
+    });
+
+    it('returns true if a given value matches a string expression', function () {
+      expect(S.testRegexp({regexp: 'val.+'}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value not matches a string expression', function () {
+      expect(S.testRegexp({regexp: 'foo.+'}, 'value')).to.be.false;
+    });
+
+    it('uses case-sensitive matching for a string expression', function () {
+      expect(S.testRegexp({regexp: 'Val.+'}, 'value')).to.be.false;
+      expect(S.testRegexp({regexp: 'Val.+'}, 'Value')).to.be.true;
+      expect(S.testRegexp({regexp: 'val.+'}, 'Value')).to.be.false;
+    });
+
+    it('returns true if a given value matches a RegExp', function () {
+      expect(S.testRegexp({regexp: /val.+/}, 'value')).to.be.true;
+    });
+
+    it('returns false if a given value matches a RegExp', function () {
+      expect(S.testRegexp({regexp: /foo.+/}, 'value')).to.be.false;
+    });
+
+    it('uses case-sensitive matching for a RegExp', function () {
+      expect(S.testRegexp({regexp: /Val.+/}, 'value')).to.be.false;
+      expect(S.testRegexp({regexp: /Val.+/}, 'Value')).to.be.true;
+      expect(S.testRegexp({regexp: /val.+/}, 'Value')).to.be.false;
+    });
+
+    it('returns false if a given value is not a string', function () {
+      expect(S.testRegexp({regexp: /val.+/}, 10)).to.be.false;
+      expect(S.testRegexp({regexp: /val.+/}, '')).to.be.false;
+    });
+
+    it('uses the "flags" option', function () {
+      expect(S.testRegexp({regexp: /Val.+/}, 'value')).to.be.false;
+      expect(S.testRegexp({regexp: 'Val.+'}, 'value')).to.be.false;
+      expect(S.testRegexp({regexp: /Val.+/, flags: 'i'}, 'value')).to.be.true;
+      expect(S.testRegexp({regexp: 'Val.+', flags: 'i'}, 'value')).to.be.true;
+    });
+
+    it('throws an error if a first argument is not an object', function () {
+      const throwable = () => S.testRegexp(10);
+      expect(throwable).to.throw(
+        'A first argument of OperatorUtils.testRegexp ' +
+          'should be an Object, but 10 given.',
+      );
+    });
+
+    it('throws an error if an operator value is a number', function () {
+      const regexp = 10;
+      const throwable = () => S.testRegexp({regexp}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is an object', function () {
+      const regexp = {};
+      const throwable = () => S.testRegexp({regexp}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if an operator value is a null', function () {
+      const regexp = null;
+      const throwable = () => S.testRegexp({regexp}, 10);
+      expect(throwable).to.throw(InvalidOperatorValueError);
+    });
+
+    it('throws an error if a "flags" value is not a string', function () {
+      const throwable = v => () =>
+        S.testRegexp({regexp: 'Val.+', flags: v}, 'val');
+      const error = v =>
+        format('RegExp flags must be a String, but %s given.', v);
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      throwable('')();
+      throwable(0)();
+      throwable(false)();
+      throwable(undefined)();
+      throwable(null)();
+    });
+  });
+});

+ 94 - 0
src/filter/order-clause-tool.js

@@ -0,0 +1,94 @@
+import {Service} from '../service/index.js';
+import {getValueByPath} from '../utils/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+
+/**
+ * Order clause tool.
+ */
+export class OrderClauseTool extends Service {
+  /**
+   Sort.
+   *
+   * @param entities
+   * @param clause
+   */
+  sort(entities, clause) {
+    if (!Array.isArray(clause)) clause = [clause];
+    const mapping = [];
+    clause.forEach((key, index) => {
+      if (typeof key !== 'string')
+        throw new InvalidArgumentError(
+          'The provided option "order" should be a String ' +
+            'or an Array of String, but %s given.',
+          key,
+        );
+      let reverse = 1;
+      const matches = key.match(/\s+(A|DE)SC$/i);
+      if (matches) {
+        key = key.replace(/\s+(A|DE)SC/i, '');
+        if (matches[1].toLowerCase() === 'de') reverse = -1;
+      }
+      mapping[index] = {key: key, reverse};
+    });
+    entities.sort(compareFn.bind(mapping));
+  }
+
+  /**
+   * Validate order clause.
+   *
+   * @param clause
+   */
+  static validateOrderClause(clause) {
+    const tempClause = Array.isArray(clause) ? clause : [clause];
+    tempClause.forEach(key => {
+      if (!key || typeof key !== 'string')
+        throw new InvalidArgumentError(
+          'The provided option "order" should be a non-empty String ' +
+            'or an Array of String, but %s given.',
+          key,
+        );
+    });
+  }
+
+  /**
+   * Normalize order clause.
+   *
+   * @param clause
+   * @return {undefined|*[]}
+   */
+  static normalizeOrderClause(clause) {
+    if (!clause) return;
+    clause = Array.isArray(clause) ? clause : [clause];
+    clause.forEach(key => {
+      if (!key || typeof key !== 'string')
+        throw new InvalidArgumentError(
+          'The provided option "order" should be a non-empty String ' +
+            'or an Array of String, but %s given.',
+          key,
+        );
+    });
+    return clause;
+  }
+}
+
+/**
+ * Compare fn.
+ *
+ * @param a
+ * @param b
+ */
+function compareFn(a, b) {
+  let undefinedA, undefinedB;
+  for (let i = 0, l = this.length; i < l; i++) {
+    const aVal = getValueByPath(a, this[i].key);
+    const bVal = getValueByPath(b, this[i].key);
+    undefinedB = bVal === undefined && aVal !== undefined;
+    undefinedA = aVal === undefined && bVal !== undefined;
+    if (undefinedB || aVal > bVal) {
+      return this[i].reverse;
+    } else if (undefinedA || aVal < bVal) {
+      return -1 * this[i].reverse;
+    }
+  }
+  return 0;
+}

+ 439 - 0
src/filter/order-clause-tool.spec.js

@@ -0,0 +1,439 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {OrderClauseTool} from './order-clause-tool.js';
+
+const S = new OrderClauseTool();
+
+describe('OrderClauseTool', function () {
+  describe('sort', function () {
+    describe('with number values', function () {
+      it('orders by a single field in ascending by default', function () {
+        const objects = [{foo: 2}, {foo: 3}, {foo: 1}, {foo: 4}];
+        S.sort(objects, 'foo');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo).to.be.eq(1);
+        expect(objects[1].foo).to.be.eq(2);
+        expect(objects[2].foo).to.be.eq(3);
+        expect(objects[3].foo).to.be.eq(4);
+      });
+
+      it('orders by a single field in descending', function () {
+        const objects = [{foo: 2}, {foo: 3}, {foo: 1}, {foo: 4}];
+        S.sort(objects, 'foo DESC');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo).to.be.eq(4);
+        expect(objects[1].foo).to.be.eq(3);
+        expect(objects[2].foo).to.be.eq(2);
+        expect(objects[3].foo).to.be.eq(1);
+      });
+
+      it('orders by a single field in ascending', function () {
+        const objects = [{foo: 2}, {foo: 3}, {foo: 1}, {foo: 4}];
+        S.sort(objects, 'foo ASC');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo).to.be.eq(1);
+        expect(objects[1].foo).to.be.eq(2);
+        expect(objects[2].foo).to.be.eq(3);
+        expect(objects[3].foo).to.be.eq(4);
+      });
+
+      it('orders by multiple fields in ascending by default', function () {
+        const objects = [
+          {foo: 2, bar: 2},
+          {foo: 2, bar: 3},
+          {foo: 2, bar: 1},
+          {foo: 1, bar: 4},
+        ];
+        S.sort(objects, ['foo', 'bar']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq(4);
+        expect(objects[1].bar).to.be.eq(1);
+        expect(objects[2].bar).to.be.eq(2);
+        expect(objects[3].bar).to.be.eq(3);
+      });
+
+      it('orders by multiple fields in descending', function () {
+        const objects = [
+          {foo: 2, bar: 2},
+          {foo: 2, bar: 3},
+          {foo: 2, bar: 1},
+          {foo: 1, bar: 4},
+        ];
+        S.sort(objects, ['foo DESC', 'bar DESC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq(3);
+        expect(objects[1].bar).to.be.eq(2);
+        expect(objects[2].bar).to.be.eq(1);
+        expect(objects[3].bar).to.be.eq(4);
+      });
+
+      it('orders by multiple fields in ascending', function () {
+        const objects = [
+          {foo: 2, bar: 2},
+          {foo: 2, bar: 3},
+          {foo: 2, bar: 1},
+          {foo: 1, bar: 4},
+        ];
+        S.sort(objects, ['foo ASC', 'bar ASC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq(4);
+        expect(objects[1].bar).to.be.eq(1);
+        expect(objects[2].bar).to.be.eq(2);
+        expect(objects[3].bar).to.be.eq(3);
+      });
+
+      it('orders by nested fields in ascending by default', function () {
+        const objects = [
+          {foo: {bar: 3}},
+          {foo: {bar: 4}},
+          {foo: {bar: 2}},
+          {foo: {bar: 1}},
+        ];
+        S.sort(objects, 'foo.bar');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo.bar).to.be.eq(1);
+        expect(objects[1].foo.bar).to.be.eq(2);
+        expect(objects[2].foo.bar).to.be.eq(3);
+        expect(objects[3].foo.bar).to.be.eq(4);
+      });
+
+      it('orders by nested fields in descending', function () {
+        const objects = [
+          {foo: {bar: 3}},
+          {foo: {bar: 4}},
+          {foo: {bar: 2}},
+          {foo: {bar: 1}},
+        ];
+        S.sort(objects, 'foo.bar DESC');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo.bar).to.be.eq(4);
+        expect(objects[1].foo.bar).to.be.eq(3);
+        expect(objects[2].foo.bar).to.be.eq(2);
+        expect(objects[3].foo.bar).to.be.eq(1);
+      });
+
+      it('orders by nested fields in ascending', function () {
+        const objects = [
+          {foo: {bar: 3}},
+          {foo: {bar: 4}},
+          {foo: {bar: 2}},
+          {foo: {bar: 1}},
+        ];
+        S.sort(objects, 'foo.bar ASC');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo.bar).to.be.eq(1);
+        expect(objects[1].foo.bar).to.be.eq(2);
+        expect(objects[2].foo.bar).to.be.eq(3);
+        expect(objects[3].foo.bar).to.be.eq(4);
+      });
+
+      it('orders by multiple fields with nested one', function () {
+        const objects = [
+          {foo: {bar: 2}, baz: 2},
+          {foo: {bar: 2}, baz: 3},
+          {foo: {bar: 2}, baz: 4},
+          {foo: {bar: 1}, baz: 1},
+        ];
+        S.sort(objects, ['foo.bar ASC', 'baz DESC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].baz).to.be.eq(1);
+        expect(objects[1].baz).to.be.eq(4);
+        expect(objects[2].baz).to.be.eq(3);
+        expect(objects[3].baz).to.be.eq(2);
+      });
+    });
+
+    describe('with string values', function () {
+      it('orders by a single field in ascending by default', function () {
+        const objects = [{foo: 'b'}, {foo: 'c'}, {foo: 'a'}, {foo: 'd'}];
+        S.sort(objects, 'foo');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo).to.be.eq('a');
+        expect(objects[1].foo).to.be.eq('b');
+        expect(objects[2].foo).to.be.eq('c');
+        expect(objects[3].foo).to.be.eq('d');
+      });
+
+      it('orders by a single field in descending', function () {
+        const objects = [{foo: 'b'}, {foo: 'c'}, {foo: 'a'}, {foo: 'd'}];
+        S.sort(objects, 'foo DESC');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo).to.be.eq('d');
+        expect(objects[1].foo).to.be.eq('c');
+        expect(objects[2].foo).to.be.eq('b');
+        expect(objects[3].foo).to.be.eq('a');
+      });
+
+      it('orders by a single field in ascending', function () {
+        const objects = [{foo: 'b'}, {foo: 'c'}, {foo: 'a'}, {foo: 'd'}];
+        S.sort(objects, 'foo ASC');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo).to.be.eq('a');
+        expect(objects[1].foo).to.be.eq('b');
+        expect(objects[2].foo).to.be.eq('c');
+        expect(objects[3].foo).to.be.eq('d');
+      });
+
+      it('orders by multiple fields in ascending by default', function () {
+        const objects = [
+          {foo: 'b', bar: 'b'},
+          {foo: 'b', bar: 'c'},
+          {foo: 'b', bar: 'a'},
+          {foo: 'a', bar: 'd'},
+        ];
+        S.sort(objects, ['foo', 'bar']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq('d');
+        expect(objects[1].bar).to.be.eq('a');
+        expect(objects[2].bar).to.be.eq('b');
+        expect(objects[3].bar).to.be.eq('c');
+      });
+
+      it('orders by multiple fields in descending', function () {
+        const objects = [
+          {foo: 'b', bar: 'b'},
+          {foo: 'b', bar: 'c'},
+          {foo: 'b', bar: 'a'},
+          {foo: 'a', bar: 'd'},
+        ];
+        S.sort(objects, ['foo DESC', 'bar DESC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq('c');
+        expect(objects[1].bar).to.be.eq('b');
+        expect(objects[2].bar).to.be.eq('a');
+        expect(objects[3].bar).to.be.eq('d');
+      });
+
+      it('orders by multiple fields in ascending', function () {
+        const objects = [
+          {foo: 'b', bar: 'b'},
+          {foo: 'b', bar: 'c'},
+          {foo: 'b', bar: 'a'},
+          {foo: 'a', bar: 'd'},
+        ];
+        S.sort(objects, ['foo ASC', 'bar ASC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq('d');
+        expect(objects[1].bar).to.be.eq('a');
+        expect(objects[2].bar).to.be.eq('b');
+        expect(objects[3].bar).to.be.eq('c');
+      });
+
+      it('orders by nested fields in ascending by default', function () {
+        const objects = [
+          {foo: {bar: 'c'}},
+          {foo: {bar: 'd'}},
+          {foo: {bar: 'b'}},
+          {foo: {bar: 'a'}},
+        ];
+        S.sort(objects, 'foo.bar');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo.bar).to.be.eq('a');
+        expect(objects[1].foo.bar).to.be.eq('b');
+        expect(objects[2].foo.bar).to.be.eq('c');
+        expect(objects[3].foo.bar).to.be.eq('d');
+      });
+
+      it('orders by nested fields in descending', function () {
+        const objects = [
+          {foo: {bar: 'c'}},
+          {foo: {bar: 'd'}},
+          {foo: {bar: 'b'}},
+          {foo: {bar: 'a'}},
+        ];
+        S.sort(objects, 'foo.bar DESC');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo.bar).to.be.eq('d');
+        expect(objects[1].foo.bar).to.be.eq('c');
+        expect(objects[2].foo.bar).to.be.eq('b');
+        expect(objects[3].foo.bar).to.be.eq('a');
+      });
+
+      it('orders by nested fields in ascending', function () {
+        const objects = [
+          {foo: {bar: 'c'}},
+          {foo: {bar: 'd'}},
+          {foo: {bar: 'b'}},
+          {foo: {bar: 'a'}},
+        ];
+        S.sort(objects, 'foo.bar ASC');
+        expect(objects).to.have.length(4);
+        expect(objects[0].foo.bar).to.be.eq('a');
+        expect(objects[1].foo.bar).to.be.eq('b');
+        expect(objects[2].foo.bar).to.be.eq('c');
+        expect(objects[3].foo.bar).to.be.eq('d');
+      });
+
+      it('orders by multiple fields with nested one', function () {
+        const objects = [
+          {foo: {bar: 'b'}, baz: 'b'},
+          {foo: {bar: 'b'}, baz: 'c'},
+          {foo: {bar: 'b'}, baz: 'd'},
+          {foo: {bar: 'a'}, baz: 'a'},
+        ];
+        S.sort(objects, ['foo.bar ASC', 'baz DESC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].baz).to.be.eq('a');
+        expect(objects[1].baz).to.be.eq('d');
+        expect(objects[2].baz).to.be.eq('c');
+        expect(objects[3].baz).to.be.eq('b');
+      });
+    });
+
+    describe('with number and string values', function () {
+      it('orders by number and string values in ascending by default', function () {
+        const objects = [
+          {foo: 2, bar: 'd'},
+          {foo: 2, bar: 'b'},
+          {foo: 2, bar: 'a'},
+          {foo: 1, bar: 'c'},
+        ];
+        S.sort(objects, ['foo', 'bar']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq('c');
+        expect(objects[1].bar).to.be.eq('a');
+        expect(objects[2].bar).to.be.eq('b');
+        expect(objects[3].bar).to.be.eq('d');
+      });
+
+      it('orders by number and string values in descending', function () {
+        const objects = [
+          {foo: 2, bar: 'd'},
+          {foo: 2, bar: 'b'},
+          {foo: 2, bar: 'a'},
+          {foo: 1, bar: 'c'},
+        ];
+        S.sort(objects, ['foo DESC', 'bar DESC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq('d');
+        expect(objects[1].bar).to.be.eq('b');
+        expect(objects[2].bar).to.be.eq('a');
+        expect(objects[3].bar).to.be.eq('c');
+      });
+
+      it('orders by number and string values in ascending', function () {
+        const objects = [
+          {foo: 2, bar: 'd'},
+          {foo: 2, bar: 'b'},
+          {foo: 2, bar: 'a'},
+          {foo: 1, bar: 'c'},
+        ];
+        S.sort(objects, ['foo ASC', 'bar ASC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq('c');
+        expect(objects[1].bar).to.be.eq('a');
+        expect(objects[2].bar).to.be.eq('b');
+        expect(objects[3].bar).to.be.eq('d');
+      });
+
+      it('orders by number and string values in mixed directions', function () {
+        const objects = [
+          {foo: 2, bar: 'd'},
+          {foo: 2, bar: 'b'},
+          {foo: 2, bar: 'a'},
+          {foo: 1, bar: 'c'},
+        ];
+        S.sort(objects, ['foo DESC', 'bar ASC']);
+        expect(objects).to.have.length(4);
+        expect(objects[0].bar).to.be.eq('a');
+        expect(objects[1].bar).to.be.eq('b');
+        expect(objects[2].bar).to.be.eq('d');
+        expect(objects[3].bar).to.be.eq('c');
+      });
+    });
+
+    it('does not throw an error if a field does not exist', function () {
+      const objects = [{foo: 1}, {foo: 2}, {foo: 3}, {foo: 4}];
+      S.sort(objects, 'bar');
+      expect(objects).to.have.length(4);
+      expect(objects[0].foo).to.be.eq(1);
+      expect(objects[1].foo).to.be.eq(2);
+      expect(objects[2].foo).to.be.eq(3);
+      expect(objects[3].foo).to.be.eq(4);
+    });
+
+    it('does not throw an error if a nested field does not exist', function () {
+      const objects = [
+        {foo: 1},
+        {foo: 2, bar: undefined},
+        {foo: 3, bar: {baz: undefined}},
+        {foo: 4, bar: {baz: 1}},
+      ];
+      S.sort(objects, 'bar.baz');
+      expect(objects).to.have.length(4);
+      expect(objects[0].foo).to.be.eq(1);
+      expect(objects[1].foo).to.be.eq(2);
+      expect(objects[2].foo).to.be.eq(3);
+      expect(objects[3].foo).to.be.eq(4);
+    });
+
+    it('throws an error if a given property is not a string', function () {
+      const throwable = () => S.sort([], 10);
+      expect(throwable).to.throw(
+        'The provided option "order" should be a String ' +
+          'or an Array of String, but 10 given.',
+      );
+    });
+  });
+
+  describe('validateOrderClause', function () {
+    it('requires a non-empty string or an array of non-empty strings', function () {
+      const validate = clause => () =>
+        OrderClauseTool.validateOrderClause(clause);
+      const error = value =>
+        format(
+          'The provided option "order" should be a non-empty String ' +
+            'or an Array of String, but %s given.',
+          value,
+        );
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate({})).to.throw(error('Object'));
+      expect(validate([''])).to.throw(error('""'));
+      expect(validate([10])).to.throw(error('10'));
+      expect(validate([true])).to.throw(error('true'));
+      expect(validate([false])).to.throw(error('false'));
+      expect(validate([undefined])).to.throw(error('undefined'));
+      expect(validate([null])).to.throw(error('null'));
+      validate('');
+      validate(false);
+      validate(undefined);
+      validate(null);
+      validate('foo');
+      validate(['foo']);
+    });
+  });
+
+  describe('normalizeOrderClause', function () {
+    it('returns an array of strings', function () {
+      const fn = OrderClauseTool.normalizeOrderClause;
+      expect(fn('foo')).to.be.eql(['foo']);
+      expect(fn(['foo'])).to.be.eql(['foo']);
+    });
+
+    it('requires a non-empty string or an array of non-empty strings', function () {
+      const fn = clause => () => OrderClauseTool.normalizeOrderClause(clause);
+      const error = value =>
+        format(
+          'The provided option "order" should be a non-empty String ' +
+            'or an Array of String, but %s given.',
+          value,
+        );
+      expect(fn(10)).to.throw(error('10'));
+      expect(fn(true)).to.throw(error('true'));
+      expect(fn({})).to.throw(error('Object'));
+      expect(fn([''])).to.throw(error('""'));
+      expect(fn([10])).to.throw(error('10'));
+      expect(fn([true])).to.throw(error('true'));
+      expect(fn([false])).to.throw(error('false'));
+      expect(fn([undefined])).to.throw(error('undefined'));
+      expect(fn([null])).to.throw(error('null'));
+      expect(fn('')()).to.be.undefined;
+      expect(fn(false)()).to.be.undefined;
+      expect(fn(undefined)()).to.be.undefined;
+      expect(fn(null)()).to.be.undefined;
+      expect(fn('foo')()).to.be.eql(['foo']);
+      expect(fn(['foo'])()).to.be.eql(['foo']);
+    });
+  });
+});

+ 64 - 0
src/filter/slice-clause-tool.js

@@ -0,0 +1,64 @@
+import {Service} from '../service/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+
+/**
+ * Slice clause tool.
+ */
+export class SliceClauseTool extends Service {
+  /**
+   * Filter.
+   *
+   * @param entities
+   * @param skip
+   * @param limit
+   */
+  slice(entities, skip, limit) {
+    if (!Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'A first argument of SliceClauseTool.slice ' +
+          'should be an Array, but %s given.',
+        entities,
+      );
+    if (skip && typeof skip !== 'number')
+      throw new InvalidArgumentError(
+        'The provided option "skip" should be a Number, but %s given.',
+        skip,
+      );
+    if (limit && typeof limit !== 'number')
+      throw new InvalidArgumentError(
+        'The provided option "limit" should be a Number, but %s given.',
+        limit,
+      );
+    skip = skip || 0;
+    limit = limit || entities.length;
+    return entities.slice(skip, skip + limit);
+  }
+
+  /**
+   * Validate skip clause.
+   *
+   * @param skip
+   */
+  static validateSkipClause(skip) {
+    if (!skip) return;
+    if (typeof skip !== 'number')
+      throw new InvalidArgumentError(
+        'The provided option "skip" should be a Number, but %s given.',
+        skip,
+      );
+  }
+
+  /**
+   * Validate limit clause.
+   *
+   * @param limit
+   */
+  static validateLimitClause(limit) {
+    if (!limit) return;
+    if (typeof limit !== 'number')
+      throw new InvalidArgumentError(
+        'The provided option "limit" should be a Number, but %s given.',
+        limit,
+      );
+  }
+}

+ 119 - 0
src/filter/slice-clause-tool.spec.js

@@ -0,0 +1,119 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {SliceClauseTool} from './slice-clause-tool.js';
+
+const S = new SliceClauseTool();
+
+describe('SliceClauseTool', function () {
+  describe('filter', function () {
+    it('does nothing if no clauses provided', function () {
+      const objects = [{id: 1}, {id: 2}, {id: 3}];
+      const result = S.slice(objects);
+      expect(result).to.be.eql(objects);
+    });
+
+    it('does nothing if a given skip is zero', function () {
+      const objects = [{id: 1}, {id: 2}, {id: 3}];
+      const result = S.slice(objects, 0);
+      expect(result).to.be.eql(objects);
+    });
+
+    it('uses a given skip to exclude array elements from start', function () {
+      const objects = [{id: 1}, {id: 2}, {id: 3}];
+      const result = S.slice(objects, 2);
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(objects[2]);
+    });
+
+    it('returns an empty array if skipping too much', function () {
+      const objects = [{id: 1}, {id: 2}, {id: 3}];
+      const result = S.slice(objects, 10);
+      expect(result).to.have.length(0);
+    });
+
+    it('does nothing if a given limit is zero', function () {
+      const objects = [{id: 1}, {id: 2}, {id: 3}];
+      const result = S.slice(objects, undefined, 0);
+      expect(result).to.be.eql(objects);
+    });
+
+    it('uses a given limit to trim a given array', function () {
+      const objects = [{id: 1}, {id: 2}, {id: 3}];
+      const result = S.slice(objects, undefined, 2);
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(objects[0]);
+      expect(result[1]).to.be.eql(objects[1]);
+    });
+
+    it('able to combine a skip and a limit option together', function () {
+      const objects = [{id: 1}, {id: 2}, {id: 3}];
+      const result = S.slice(objects, 1, 1);
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(objects[1]);
+    });
+
+    it('throws an error if a first argument is not an array', function () {
+      const throwable = () => S.slice(10);
+      expect(throwable).to.throw(
+        'A first argument of SliceClauseTool.slice ' +
+          'should be an Array, but 10 given.',
+      );
+    });
+
+    it('throws an error if the given "skip" option is not a number', function () {
+      const throwable = () => S.slice([], 'invalid');
+      expect(throwable).to.throw(
+        'The provided option "skip" should be a Number, but "invalid" given.',
+      );
+    });
+
+    it('throws an error if the given "limit" option is not a number', function () {
+      const throwable = () => S.slice([], undefined, 'invalid');
+      expect(throwable).to.throw(
+        'The provided option "limit" should be a Number, but "invalid" given.',
+      );
+    });
+  });
+
+  describe('validateSkipClause', function () {
+    it('requires a number value or a falsy value', function () {
+      const validate = clause => () =>
+        SliceClauseTool.validateSkipClause(clause);
+      const error = value =>
+        format(
+          'The provided option "skip" should be a Number, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      validate('');
+      validate(false);
+      validate(undefined);
+      validate(null);
+      validate(10);
+      validate(0);
+    });
+  });
+
+  describe('validateLimitClause', function () {
+    it('requires a number value or a falsy value', function () {
+      const validate = clause => () =>
+        SliceClauseTool.validateLimitClause(clause);
+      const error = value =>
+        format(
+          'The provided option "limit" should be a Number, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      validate('');
+      validate(false);
+      validate(undefined);
+      validate(null);
+      validate(10);
+      validate(0);
+    });
+  });
+});

+ 159 - 0
src/filter/where-clause-tool.js

@@ -0,0 +1,159 @@
+import {Service} from '../service/index.js';
+import {getValueByPath} from '../utils/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {OperatorClauseTool} from './operator-clause-tool.js';
+
+/**
+ * Where clause tool.
+ */
+export class WhereClauseTool extends Service {
+  /**
+   * Filter by where clause.
+   *
+   * @example
+   * ```
+   * const entities = [
+   *   {foo: 1, bar: 'a'},
+   *   {foo: 2, bar: 'b'},
+   *   {foo: 3, bar: 'b'},
+   *   {foo: 4, bar: 'b'},
+   * ];
+   *
+   * const result = filterByWhereClause(entities, {
+   *   foo: {gt: 2},
+   *   bar: 'b',
+   * });
+   *
+   * console.log(result);
+   * // [
+   * //   {foo: 3, bar: 'b'},
+   * //   {foo: 4, bar: 'b'},
+   * // ];
+   *
+   * ```
+   *
+   * @param entities
+   * @param where
+   */
+  filter(entities, where = {}) {
+    if (!Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'A first argument of WhereUtils.filter ' +
+          'should be an Array of Objects, but %s given.',
+        entities,
+      );
+    return entities.filter(this._createFilter(where));
+  }
+
+  /**
+   * Create where filter.
+   *
+   * @param whereClause
+   */
+  _createFilter(whereClause) {
+    if (typeof whereClause === 'function') return whereClause;
+    if (typeof whereClause !== 'object')
+      throw new InvalidArgumentError(
+        'The provided option "where" should be an Object, but %s given.',
+        whereClause,
+      );
+    const keys = Object.keys(whereClause);
+    return data => {
+      if (typeof data !== 'object')
+        throw new InvalidArgumentError(
+          'A first argument of WhereUtils.filter ' +
+            'should be an Array of Objects, but %s given.',
+          data,
+        );
+      return keys.every(key => {
+        // AndClause (recursion)
+        if (key === 'and' && key in whereClause) {
+          const andClause = whereClause[key];
+          if (Array.isArray(andClause))
+            return andClause.every(clause => this._createFilter(clause)(data));
+          // OrClause (recursion)
+        } else if (key === 'or' && key in whereClause) {
+          const orClause = whereClause[key];
+          if (Array.isArray(orClause))
+            return orClause.some(clause => this._createFilter(clause)(data));
+        }
+        // PropertiesClause (properties)
+        const value = getValueByPath(data, key);
+        const matcher = whereClause[key];
+        // Property value is an array.
+        if (Array.isArray(value)) {
+          // {neq: ...}
+          if (
+            typeof matcher === 'object' &&
+            matcher !== null &&
+            'neq' in matcher &&
+            matcher.neq !== undefined
+          ) {
+            // The following condition is for the case where
+            // we are querying with a neq filter, and when
+            // the value is an empty array ([]).
+            if (value.length === 0) return true;
+            // The neq operator requires each element
+            // of the array to be excluded.
+            return value.every((el, index) => {
+              const where = {};
+              where[index] = matcher;
+              return this._createFilter(where)({...value});
+            });
+          }
+          // Requires one of an array elements to be match.
+          return value.some((el, index) => {
+            const where = {};
+            where[index] = matcher;
+            return this._createFilter(where)({...value});
+          });
+        }
+        // Test property value.
+        if (this._test(matcher, value)) return true;
+      });
+    };
+  }
+
+  /**
+   * Value testing.
+   *
+   * @param example
+   * @param value
+   */
+  _test(example, value) {
+    // Test null.
+    if (example == null) {
+      return value == null;
+    }
+    // Test RegExp.
+    // noinspection ALL
+    if (example instanceof RegExp) {
+      if (typeof value === 'string') return !!value.match(example);
+      return false;
+    }
+    // Operator clause.
+    if (typeof example === 'object') {
+      const operatorsTest = this.get(OperatorClauseTool).testAll(
+        example,
+        value,
+      );
+      if (operatorsTest !== undefined) return operatorsTest;
+    }
+    // Not strict equality.
+    return example == value;
+  }
+
+  /**
+   * Validate where clause.
+   *
+   * @param clause
+   */
+  static validateWhereClause(clause) {
+    if (!clause) return;
+    if (typeof clause !== 'object' || Array.isArray(clause))
+      throw new InvalidArgumentError(
+        'The provided option "where" should be an Object, but %s given.',
+        clause,
+      );
+  }
+}

+ 281 - 0
src/filter/where-clause-tool.spec.js

@@ -0,0 +1,281 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {WhereClauseTool} from './where-clause-tool.js';
+
+const S = new WhereClauseTool();
+
+const OBJECTS = [
+  {
+    id: 1,
+    name: 'John',
+    surname: 'Doe',
+    age: 21,
+    hobbies: ['bicycle', 'yoga'],
+    nickname: 'Spear',
+    birthdate: '2002-04-14',
+  },
+  {
+    id: 2,
+    name: 'Mary',
+    surname: 'Smith',
+    age: 21,
+    hobbies: ['yoga', 'meditation'],
+    nickname: 'Flower',
+    birthdate: '2002-01-12',
+  },
+  {
+    id: 3,
+    name: 'James',
+    surname: 'Smith',
+    age: 21,
+    hobbies: [],
+    nickname: null,
+    birthdate: '2002-03-01',
+  },
+  {
+    id: 4,
+    name: 'Oliver',
+    surname: 'Smith',
+    age: 32,
+    hobbies: ['bicycle'],
+    birthdate: '1991-06-24',
+  },
+];
+
+describe('WhereClauseTool', function () {
+  describe('filter', function () {
+    it('returns the same array if no given condition', function () {
+      const result = S.filter(OBJECTS);
+      expect(result).to.be.eql(OBJECTS);
+    });
+
+    it('returns a filtered array by matched properties', function () {
+      const result = S.filter(OBJECTS, {surname: 'Smith', age: 21});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[1]);
+      expect(result[1]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('the and operator requires each given condition to be met', function () {
+      const result = S.filter(OBJECTS, {
+        and: [{name: 'James'}, {age: 21}],
+      });
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('the or operator requires one of a given condition to be met', function () {
+      const result = S.filter(OBJECTS, {
+        or: [{name: 'James'}, {age: 21}],
+      });
+      expect(result).to.have.length(3);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[1]);
+      expect(result[2]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses property value to match an array value', function () {
+      const result = S.filter(OBJECTS, {hobbies: 'yoga'});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[1]);
+    });
+
+    it('uses given RegExp to match a property value', function () {
+      const result = S.filter(OBJECTS, {surname: /^Sm.+/});
+      expect(result).to.have.length(3);
+      expect(result[0]).to.be.eql(OBJECTS[1]);
+      expect(result[1]).to.be.eql(OBJECTS[2]);
+      expect(result[2]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses given RegExp to match an array value', function () {
+      const result = S.filter(OBJECTS, {hobbies: /^\w+cycle/});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('skips not supported values for RegExp', function () {
+      const result = S.filter(OBJECTS, {id: /test/});
+      expect(result).to.be.empty;
+    });
+
+    it('uses an "eq" operator to match equality', function () {
+      const result = S.filter(OBJECTS, {name: {eq: 'John'}});
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+    });
+
+    it('uses a "neq" operator to match non-equality', function () {
+      const result = S.filter(OBJECTS, {name: {neq: 'John'}});
+      expect(result).to.have.length(3);
+      expect(result[0]).to.be.eql(OBJECTS[1]);
+      expect(result[1]).to.be.eql(OBJECTS[2]);
+      expect(result[2]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses a "neq" operator to match an empty array', function () {
+      const result = S.filter(OBJECTS, {hobbies: {neq: 'bicycle'}});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[1]);
+      expect(result[1]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses a "gt" operator to compare values', function () {
+      const result = S.filter(OBJECTS, {id: {gt: 2}});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[2]);
+      expect(result[1]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses a "gte" operator to compare values', function () {
+      const result = S.filter(OBJECTS, {id: {gte: 2}});
+      expect(result).to.have.length(3);
+      expect(result[0]).to.be.eql(OBJECTS[1]);
+      expect(result[1]).to.be.eql(OBJECTS[2]);
+      expect(result[2]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses a "lt" operator to compare values', function () {
+      const result = S.filter(OBJECTS, {id: {lt: 3}});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[1]);
+    });
+
+    it('uses a "lte" operator to compare values', function () {
+      const result = S.filter(OBJECTS, {id: {lte: 3}});
+      expect(result).to.have.length(3);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[1]);
+      expect(result[2]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses a "inq" operator to compare values', function () {
+      const result = S.filter(OBJECTS, {id: {inq: [2, 3]}});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[1]);
+      expect(result[1]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses a "nin" operator to compare values', function () {
+      const result = S.filter(OBJECTS, {id: {nin: [2, 3]}});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses a "between" operator to compare values', function () {
+      const result = S.filter(OBJECTS, {id: {between: [2, 3]}});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[1]);
+      expect(result[1]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses an "exists" operator to check existence', function () {
+      const result = S.filter(OBJECTS, {nickname: {exists: true}});
+      expect(result).to.have.length(3);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[1]);
+      expect(result[2]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses an "exists" operator to check non-existence', function () {
+      const result = S.filter(OBJECTS, {nickname: {exists: false}});
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses a "like" operator to match by a substring', function () {
+      const result = S.filter(OBJECTS, {name: {like: 'liv'}});
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses a "nlike" operator to exclude by a substring', function () {
+      const result = S.filter(OBJECTS, {name: {nlike: 'liv'}});
+      expect(result).to.have.length(3);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[1]);
+      expect(result[2]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses a "ilike" operator to case-insensitively matching by a substring', function () {
+      const result = S.filter(OBJECTS, {name: {ilike: 'LIV'}});
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses a "nilike" operator to exclude case-insensitively by a substring', function () {
+      const result = S.filter(OBJECTS, {name: {nilike: 'LIV'}});
+      expect(result).to.have.length(3);
+      expect(result[0]).to.be.eql(OBJECTS[0]);
+      expect(result[1]).to.be.eql(OBJECTS[1]);
+      expect(result[2]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses a "regexp" operator to compare values', function () {
+      const result = S.filter(OBJECTS, {name: {regexp: '^Jam.*'}});
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(OBJECTS[2]);
+    });
+
+    it('uses a null to match an undefined and null value', function () {
+      const result = S.filter(OBJECTS, {nickname: null});
+      expect(result).to.have.length(2);
+      expect(result[0]).to.be.eql(OBJECTS[2]);
+      expect(result[1]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('uses a given function to filter values', function () {
+      const result = S.filter(OBJECTS, v => v.nickname === 'Flower');
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(OBJECTS[1]);
+    });
+
+    it('throws an error if a first argument is not an Array', function () {
+      const throwable = () => S.filter(10);
+      expect(throwable).to.throw(
+        'A first argument of WhereUtils.filter ' +
+          'should be an Array of Objects, but 10 given.',
+      );
+    });
+
+    it('throws an error if elements of a first argument is not an Object', function () {
+      const throwable = () => S.filter([10]);
+      expect(throwable).to.throw(
+        'A first argument of WhereUtils.filter ' +
+          'should be an Array of Objects, but 10 given.',
+      );
+    });
+
+    it('throws an error if a provided second argument is not an Object', function () {
+      const throwable = () => S.filter([], 10);
+      expect(throwable).to.throw(
+        'The provided option "where" should be an Object, but 10 given.',
+      );
+    });
+  });
+
+  describe('validateWhereClause', function () {
+    it('requires an object value or a falsy value', function () {
+      const validate = clause => () =>
+        WhereClauseTool.validateWhereClause(clause);
+      const error = value =>
+        format(
+          'The provided option "where" should be an Object, but %s given.',
+          value,
+        );
+      expect(validate('str')).to.throw(error('"str"'));
+      expect(validate(10)).to.throw(error('10'));
+      expect(validate(true)).to.throw(error('true'));
+      expect(validate([])).to.throw(error('Array'));
+      validate('');
+      validate(false);
+      validate(undefined);
+      validate(null);
+      validate({});
+    });
+  });
+});

+ 9 - 0
src/index.js

@@ -0,0 +1,9 @@
+export * from './schema.js';
+export * from './utils/index.js';
+export * from './errors/index.js';
+export * from './filter/index.js';
+export * from './adapter/index.js';
+export * from './service/index.js';
+export * from './relations/index.js';
+export * from './definition/index.js';
+export * from './repository/index.js';

+ 236 - 0
src/relations/belongs-to-resolver.js

@@ -0,0 +1,236 @@
+import {Service} from '../service/index.js';
+import {cloneDeep} from '../utils/index.js';
+import {singularize} from '../utils/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {RepositoryRegistry} from '../repository/index.js';
+import {ModelDefinitionUtils} from '../definition/index.js';
+
+/**
+ * Belongs to resolver.
+ */
+export class BelongsToResolver extends Service {
+  /**
+   * Include to.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} targetName
+   * @param {string} relationName
+   * @param {string|undefined} foreignKey
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includeTo(
+    entities,
+    sourceName,
+    targetName,
+    relationName,
+    foreignKey = undefined,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of BelongsToResolver.includeTo requires ' +
+          'an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of BelongsToResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!targetName || typeof targetName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetName" of BelongsToResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        targetName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of BelongsToResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        relationName,
+      );
+    if (foreignKey && typeof foreignKey !== 'string')
+      throw new InvalidArgumentError(
+        'The provided parameter "foreignKey" of BelongsToResolver.includeTo ' +
+          'should be a String, but %s given.',
+        foreignKey,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of BelongsToResolver.includeTo ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+    if (foreignKey == null) foreignKey = `${relationName}Id`;
+    const targetIds = entities.reduce((acc, entity) => {
+      if (!entity || typeof entity !== 'object' || Array.isArray(entity))
+        throw new InvalidArgumentError(
+          'The parameter "entities" of BelongsToResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          entity,
+        );
+      const targetId = entity[foreignKey];
+      return targetId != null ? [...acc, targetId] : acc;
+    }, []);
+    const targetRepository =
+      this.get(RepositoryRegistry).getRepository(targetName);
+    const targetPkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(targetName);
+    scope = scope ? cloneDeep(scope) : {};
+    const filter = cloneDeep(scope);
+    filter.where = {
+      and: [
+        {[targetPkPropName]: {inq: targetIds}},
+        ...(scope.where ? [scope.where] : []),
+      ],
+    };
+    const targets = await targetRepository.find(filter);
+    entities.forEach(entity => {
+      const target = targets.find(
+        e => e[targetPkPropName] === entity[foreignKey],
+      );
+      if (target) entity[relationName] = target;
+    });
+  }
+
+  /**
+   * Include polymorphic to.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} relationName
+   * @param {string|undefined} foreignKey
+   * @param {string|undefined} discriminator
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includePolymorphicTo(
+    entities,
+    sourceName,
+    relationName,
+    foreignKey = undefined,
+    discriminator = undefined,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of BelongsToResolver.includePolymorphicTo ' +
+          'requires an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of BelongsToResolver.includePolymorphicTo ' +
+          'requires a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of BelongsToResolver.includePolymorphicTo ' +
+          'requires a non-empty String, but %s given.',
+        relationName,
+      );
+    if (foreignKey && typeof foreignKey !== 'string')
+      throw new InvalidArgumentError(
+        'The provided parameter "foreignKey" of BelongsToResolver.includePolymorphicTo ' +
+          'should be a String, but %s given.',
+        foreignKey,
+      );
+    if (discriminator && typeof discriminator !== 'string')
+      throw new InvalidArgumentError(
+        'The provided parameter "discriminator" of BelongsToResolver.includePolymorphicTo ' +
+          'should be a String, but %s given.',
+        discriminator,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of BelongsToResolver.includePolymorphicTo ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+    if (foreignKey == null) {
+      const singularRelationName = singularize(relationName);
+      foreignKey = `${singularRelationName}Id`;
+    }
+    if (discriminator == null) {
+      const singularRelationName = singularize(relationName);
+      discriminator = `${singularRelationName}Type`;
+    }
+    const targetIdsByTargetName = {};
+    entities.forEach(entity => {
+      if (!entity || typeof entity !== 'object' || Array.isArray(entity))
+        throw new InvalidArgumentError(
+          'The parameter "entities" of BelongsToResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          entity,
+        );
+      const targetId = entity[foreignKey];
+      const targetName = entity[discriminator];
+      if (targetId == null || targetName == null) return;
+      if (targetIdsByTargetName[targetName] == null)
+        targetIdsByTargetName[targetName] = [];
+      if (!targetIdsByTargetName[targetName].includes(targetId))
+        targetIdsByTargetName[targetName].push(targetId);
+    });
+    const promises = [];
+    const targetNames = Object.keys(targetIdsByTargetName);
+    scope = scope ? cloneDeep(scope) : {};
+    const targetEntitiesByTargetNames = {};
+    targetNames.forEach(targetName => {
+      let targetRepository;
+      try {
+        targetRepository =
+          this.get(RepositoryRegistry).getRepository(targetName);
+      } catch (error) {
+        if (error instanceof InvalidArgumentError) {
+          if (
+            error.message === `The model "${targetName}" is not defined.` ||
+            error.message ===
+              `The model "${targetName}" does not have a specified datasource.`
+          ) {
+            return;
+          }
+        } else {
+          throw error;
+        }
+      }
+      const targetPkPropName =
+        this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(targetName);
+      const targetFilter = cloneDeep(scope);
+      const targetIds = targetIdsByTargetName[targetName];
+      targetFilter.where = {
+        and: [
+          {[targetPkPropName]: {inq: targetIds}},
+          ...(scope.where ? [scope.where] : []),
+        ],
+      };
+      const promise = targetRepository.find(targetFilter).then(result => {
+        targetEntitiesByTargetNames[targetName] = [
+          ...(targetEntitiesByTargetNames[targetName] ?? []),
+          ...result,
+        ];
+      });
+      promises.push(promise);
+    });
+    await Promise.all(promises);
+    entities.forEach(entity => {
+      const targetId = entity[foreignKey];
+      const targetName = entity[discriminator];
+      if (
+        targetId == null ||
+        targetName == null ||
+        targetEntitiesByTargetNames[targetName] == null
+      ) {
+        return;
+      }
+      const targetEntities = targetEntitiesByTargetNames[targetName] ?? [];
+      const targetPkPropName =
+        this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(targetName);
+      const target = targetEntities.find(e => e[targetPkPropName] === targetId);
+      if (target) entity[relationName] = target;
+    });
+  }
+}

+ 1047 - 0
src/relations/belongs-to-resolver.spec.js

@@ -0,0 +1,1047 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {Schema} from '../schema.js';
+import {DataType} from '../definition/index.js';
+import {RelationType} from '../definition/index.js';
+import {BelongsToResolver} from './belongs-to-resolver.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../definition/index.js';
+
+describe('BelongsToResolver', function () {
+  describe('includeTo', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of BelongsToResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo(v, 'sourceName', 'targetName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of BelongsToResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([v], 'sourceName', 'targetName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of BelongsToResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v => R.includeTo([], v, 'targetName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "targetName" of BelongsToResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v => R.includeTo([], 'sourceName', v, 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of BelongsToResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v => R.includeTo([], 'sourceName', 'targetName', v);
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "foreignKey" to be a string', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The provided parameter "foreignKey" of BelongsToResolver.includeTo ' +
+            'should be a String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], 'sourceName', 'targetName', 'relationName', v);
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of BelongsToResolver.includeTo ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          undefined,
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('throws an error if the given target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(BelongsToResolver);
+      const promise = R.includeTo([], 'source', 'target', 'relation');
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" is not defined',
+      );
+    });
+
+    it('throws an error if the given target model does not have a datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'target'});
+      const R = S.get(BelongsToResolver);
+      const promise = R.includeTo([], 'source', 'target', 'relation');
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have a specified datasource.',
+      );
+    });
+
+    it('does not throw an error if a relation target is not found', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({parentId: 10});
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([source], 'source', 'target', 'parent');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: 10,
+      });
+    });
+
+    it('includes if a primary key is not defined in the target model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({});
+      expect(target).to.be.eql({[DEF_PK]: target[DEF_PK]});
+      const source = await sourceRep.create({parentId: target[DEF_PK]});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([source], 'source', 'target', 'parent');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parent: target,
+      });
+    });
+
+    it('includes if the target model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({});
+      expect(target).to.be.eql({myId: target.myId});
+      const source = await sourceRep.create({parentId: target.myId});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target.myId,
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([source], 'source', 'target', 'parent');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target.myId,
+        parent: target,
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({});
+      expect(target).to.be.eql({[DEF_PK]: target[DEF_PK]});
+      const source = await sourceRep.create({parentId: target[DEF_PK]});
+      expect(source).to.be.eql({
+        myId: source.myId,
+        parentId: target[DEF_PK],
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([source], 'source', 'target', 'parent');
+      expect(source).to.be.eql({
+        myId: source.myId,
+        parentId: target[DEF_PK],
+        parent: target,
+      });
+    });
+
+    it('includes if the property "foreignKey" is specified', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({});
+      expect(target).to.be.eql({[DEF_PK]: target[DEF_PK]});
+      const source = await sourceRep.create({parentId: target[DEF_PK]});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([source], 'source', 'target', 'relation', 'parentId');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        relation: target,
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      const source = await sourceRep.create({
+        parentId: target[DEF_PK],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([source], 'source', 'target', 'parent', undefined, {
+        where: {foo: 'barVal'},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+      });
+      await R.includeTo([source], 'source', 'target', 'parent', undefined, {
+        where: {foo: 'fooVal'},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parent: target,
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      const source = await sourceRep.create({
+        parentId: target[DEF_PK],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([source], 'source', 'target', 'parent', undefined, {
+        fields: [DEF_PK, 'bar'],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parent: {
+          [DEF_PK]: target[DEF_PK],
+          bar: target.bar,
+        },
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelB',
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a = await aRep.create({});
+      const b = await bRep.create({parentId: a.id});
+      const c = await cRep.create({parentId: b.id});
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+      });
+      expect(b).to.be.eql({
+        id: b.id,
+        source: 'modelB',
+        parentId: a.id,
+      });
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentId: b.id,
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([c], 'modelC', 'modelB', 'parent', undefined, {
+        include: 'parent',
+      });
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentId: b.id,
+        parent: {
+          id: b.id,
+          source: 'modelB',
+          parentId: a.id,
+          parent: {
+            id: a.id,
+            source: 'modelA',
+          },
+        },
+      });
+    });
+
+    it('does not break the "and" operator of the given "where" clause', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      const source = await sourceRep.create({
+        parentId: target[DEF_PK],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includeTo([source], 'source', 'target', 'parent', undefined, {
+        where: {and: [{foo: 'barVal'}]},
+        fields: [DEF_PK, 'bar'],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+      });
+      await R.includeTo([source], 'source', 'target', 'parent', undefined, {
+        where: {and: [{foo: 'fooVal'}]},
+        fields: [DEF_PK, 'bar'],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parent: {
+          [DEF_PK]: target[DEF_PK],
+          bar: target.bar,
+        },
+      });
+    });
+  });
+
+  describe('includePolymorphicTo', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of BelongsToResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(v, 'sourceName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of BelongsToResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo([v], 'sourceName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of BelongsToResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo([], v, 'sourceName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of BelongsToResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v => R.includePolymorphicTo([], 'sourceName', v);
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "foreignKey" to be a string', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The provided parameter "foreignKey" of BelongsToResolver.includePolymorphicTo ' +
+            'should be a String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo([], 'sourceName', 'relationName', v);
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+    });
+
+    it('requires the provided parameter "discriminator" to be a string', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The provided parameter "discriminator" of BelongsToResolver.includePolymorphicTo ' +
+            'should be a String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo([], 'sourceName', 'relationName', undefined, v);
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(BelongsToResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of BelongsToResolver.includePolymorphicTo ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'relationName',
+          undefined,
+          undefined,
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('does not throw an error if a target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(BelongsToResolver);
+      const entity = {[DEF_PK]: 1, parentId: 1, parentType: 'target'};
+      await R.includePolymorphicTo([entity], 'source', 'parent');
+      expect(entity).to.be.eql(entity);
+    });
+
+    it('does not throws an error if a target model does not have datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({name: 'target'});
+      const R = S.get(BelongsToResolver);
+      const entity = {[DEF_PK]: 1, parentId: 1, parentType: 'target'};
+      await R.includePolymorphicTo([entity], 'source', 'parent');
+      expect(entity).to.be.eql(entity);
+    });
+
+    it('does not throw an error if a relation target is not found', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({
+        parentId: 10,
+        parentType: 'target',
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includePolymorphicTo([source], 'source', 'parent');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: 10,
+        parentType: 'target',
+      });
+    });
+
+    it('does not throw an error if no discriminator value', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(BelongsToResolver);
+      const entity = {[DEF_PK]: 1, parentId: 1};
+      await R.includePolymorphicTo([entity], 'source', 'parent');
+      expect(entity).to.be.eql(entity);
+    });
+
+    it('includes if a primary key is not defined in the target model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({});
+      expect(target).to.be.eql({[DEF_PK]: target[DEF_PK]});
+      const source = await sourceRep.create({
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includePolymorphicTo([source], 'source', 'parent');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parentType: 'target',
+        parent: target,
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({});
+      expect(target).to.be.eql({[DEF_PK]: target[DEF_PK]});
+      const source = await sourceRep.create({
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      expect(source).to.be.eql({
+        myId: source.myId,
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includePolymorphicTo([source], 'source', 'parent');
+      expect(source).to.be.eql({
+        myId: source.myId,
+        parentId: target[DEF_PK],
+        parentType: 'target',
+        parent: target,
+      });
+    });
+
+    it('includes if the property "foreignKey" is specified', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({});
+      expect(target).to.be.eql({[DEF_PK]: target[DEF_PK]});
+      const source = await sourceRep.create({
+        parentId: target[DEF_PK],
+        relationType: 'target',
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        relationType: 'target',
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includePolymorphicTo([source], 'source', 'relation', 'parentId');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        relationType: 'target',
+        relation: target,
+      });
+    });
+
+    it('includes if the property "discriminator" is specified', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({});
+      expect(target).to.be.eql({[DEF_PK]: target[DEF_PK]});
+      const source = await sourceRep.create({
+        relationId: target[DEF_PK],
+        parentType: 'target',
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        relationId: target[DEF_PK],
+        parentType: 'target',
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'relation',
+        undefined,
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        relationId: target[DEF_PK],
+        parentType: 'target',
+        relation: target,
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      const source = await sourceRep.create({
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'parent',
+        undefined,
+        undefined,
+        {where: {foo: 'barVal'}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'parent',
+        undefined,
+        undefined,
+        {where: {foo: 'fooVal'}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parentType: 'target',
+        parent: target,
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target = await targetRep.create({
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        foo: 'fooVal',
+        bar: 'barVal',
+      });
+      const source = await sourceRep.create({
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parentType: 'target',
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'parent',
+        undefined,
+        undefined,
+        {fields: [DEF_PK, 'bar']},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentId: target[DEF_PK],
+        parentType: 'target',
+        parent: {
+          [DEF_PK]: target[DEF_PK],
+          bar: target.bar,
+        },
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a = await aRep.create({});
+      const b = await bRep.create({parentId: a.id});
+      const c = await cRep.create({parentId: b.id, parentType: 'modelB'});
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+      });
+      expect(b).to.be.eql({
+        id: b.id,
+        source: 'modelB',
+        parentId: a.id,
+      });
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentId: b.id,
+        parentType: 'modelB',
+      });
+      const R = S.get(BelongsToResolver);
+      await R.includePolymorphicTo(
+        [c],
+        'modelC',
+        'parent',
+        undefined,
+        undefined,
+        {include: 'parent'},
+      );
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentId: b.id,
+        parentType: 'modelB',
+        parent: {
+          id: b.id,
+          source: 'modelB',
+          parentId: a.id,
+          parent: {
+            id: a.id,
+            source: 'modelA',
+          },
+        },
+      });
+    });
+  });
+});

+ 313 - 0
src/relations/has-many-resolver.js

@@ -0,0 +1,313 @@
+import {Service} from '../service/index.js';
+import {cloneDeep} from '../utils/index.js';
+import {RelationType} from '../definition/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {RepositoryRegistry} from '../repository/index.js';
+import {ModelDefinitionUtils} from '../definition/index.js';
+
+/**
+ * Has many resolver.
+ */
+export class HasManyResolver extends Service {
+  /**
+   * Include to.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} targetName
+   * @param {string} relationName
+   * @param {string} foreignKey
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includeTo(
+    entities,
+    sourceName,
+    targetName,
+    relationName,
+    foreignKey,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of HasManyResolver.includeTo requires ' +
+          'an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of HasManyResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!targetName || typeof targetName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetName" of HasManyResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        targetName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of HasManyResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        relationName,
+      );
+    if (!foreignKey || typeof foreignKey !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "foreignKey" of HasManyResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        foreignKey,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of HasManyResolver.includeTo ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+
+    const sourcePkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(sourceName);
+    const sourceIds = [];
+    entities.forEach(entity => {
+      if (!entity || typeof entity !== 'object' || Array.isArray(entity))
+        throw new InvalidArgumentError(
+          'The parameter "entities" of HasManyResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          entity,
+        );
+      const sourceId = entity[sourcePkPropName];
+      if (sourceIds.includes(sourceId)) return;
+      sourceIds.push(sourceId);
+    });
+
+    const promises = [];
+    const targetRepository =
+      this.get(RepositoryRegistry).getRepository(targetName);
+    scope = scope ? cloneDeep(scope) : {};
+    const targetsBySourceId = new Map();
+    sourceIds.forEach(sourceId => {
+      const filter = cloneDeep(scope);
+      filter.where = {
+        and: [{[foreignKey]: sourceId}, ...(scope.where ? [scope.where] : [])],
+      };
+      promises.push(
+        targetRepository.find(filter).then(result => {
+          if (result.length) {
+            let targets = targetsBySourceId.get(sourceId) ?? [];
+            targets = [...targets, ...result];
+            targetsBySourceId.set(sourceId, targets);
+          }
+        }),
+      );
+    });
+    await Promise.all(promises);
+
+    entities.forEach(entity => {
+      const sourceId = entity[sourcePkPropName];
+      entity[relationName] = targetsBySourceId.get(sourceId) ?? [];
+    });
+  }
+
+  /**
+   * Include polymorphic to.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} targetName
+   * @param {string} relationName
+   * @param {string} foreignKey
+   * @param {string} discriminator
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includePolymorphicTo(
+    entities,
+    sourceName,
+    targetName,
+    relationName,
+    foreignKey,
+    discriminator,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of HasManyResolver.includePolymorphicTo requires ' +
+          'an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of HasManyResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!targetName || typeof targetName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetName" of HasManyResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        targetName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of HasManyResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        relationName,
+      );
+    if (!foreignKey || typeof foreignKey !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "foreignKey" of HasManyResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        foreignKey,
+      );
+    if (!discriminator || typeof discriminator !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "discriminator" of HasManyResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        discriminator,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of HasManyResolver.includePolymorphicTo ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+
+    const sourcePkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(sourceName);
+    const sourceIds = [];
+    entities.forEach(entity => {
+      if (!entity || typeof entity !== 'object' || Array.isArray(entity))
+        throw new InvalidArgumentError(
+          'The parameter "entities" of HasManyResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          entity,
+        );
+      const sourceId = entity[sourcePkPropName];
+      if (sourceIds.includes(sourceId)) return;
+      sourceIds.push(sourceId);
+    });
+
+    const promises = [];
+    const targetRepository =
+      this.get(RepositoryRegistry).getRepository(targetName);
+    scope = scope ? cloneDeep(scope) : {};
+    const targetsBySourceId = new Map();
+    sourceIds.forEach(sourceId => {
+      const filter = cloneDeep(scope);
+      filter.where = {
+        and: [
+          {[foreignKey]: sourceId, [discriminator]: sourceName},
+          ...(scope.where ? [scope.where] : []),
+        ],
+      };
+      promises.push(
+        targetRepository.find(filter).then(result => {
+          if (result.length) {
+            let targets = targetsBySourceId.get(sourceId) ?? [];
+            targets = [...targets, ...result];
+            targetsBySourceId.set(sourceId, targets);
+          }
+        }),
+      );
+    });
+    await Promise.all(promises);
+
+    entities.forEach(entity => {
+      const sourceId = entity[sourcePkPropName];
+      entity[relationName] = targetsBySourceId.get(sourceId) ?? [];
+    });
+  }
+
+  /**
+   * Include polymorphic by relation name.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} targetName
+   * @param {string} relationName
+   * @param {string} targetRelationName
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includePolymorphicByRelationName(
+    entities,
+    sourceName,
+    targetName,
+    relationName,
+    targetRelationName,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of HasManyResolver.includePolymorphicByRelationName requires ' +
+          'an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of HasManyResolver.includePolymorphicByRelationName requires ' +
+          'a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!targetName || typeof targetName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetName" of HasManyResolver.includePolymorphicByRelationName requires ' +
+          'a non-empty String, but %s given.',
+        targetName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of HasManyResolver.includePolymorphicByRelationName requires ' +
+          'a non-empty String, but %s given.',
+        relationName,
+      );
+    if (!targetRelationName || typeof targetRelationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetRelationName" of HasManyResolver.includePolymorphicByRelationName requires ' +
+          'a non-empty String, but %s given.',
+        targetRelationName,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of HasManyResolver.includePolymorphicByRelationName ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+
+    const targetRelationDef = this.get(
+      ModelDefinitionUtils,
+    ).getRelationDefinitionByName(targetName, targetRelationName);
+    if (targetRelationDef.type !== RelationType.BELONGS_TO)
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s is a polymorphic "hasMany" relation, ' +
+          'so it requires the target relation %s to be a polymorphic "belongsTo", ' +
+          'but %s type given.',
+        relationName,
+        sourceName,
+        targetRelationName,
+        targetRelationDef.type,
+      );
+    if (!targetRelationDef.polymorphic)
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s is a polymorphic "hasMany" relation, ' +
+          'so it requires the target relation %s to be a polymorphic too.',
+        relationName,
+        sourceName,
+        targetRelationName,
+      );
+    const foreignKey =
+      targetRelationDef.foreignKey || `${targetRelationName}Id`;
+    const discriminator =
+      targetRelationDef.discriminator || `${targetRelationName}Type`;
+
+    return this.includePolymorphicTo(
+      entities,
+      sourceName,
+      targetName,
+      relationName,
+      foreignKey,
+      discriminator,
+      scope,
+    );
+  }
+}

+ 2911 - 0
src/relations/has-many-resolver.spec.js

@@ -0,0 +1,2911 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {Schema} from '../schema.js';
+import {DataType} from '../definition/index.js';
+import {RelationType} from '../definition/index.js';
+import {HasManyResolver} from './has-many-resolver.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../definition/index.js';
+
+describe('HasManyResolver', function () {
+  describe('includeTo', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasManyResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo(
+          v,
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasManyResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([v], 'source', 'target', 'relationName', 'foreignKey');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of HasManyResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], v, 'targetName', 'relationName', 'foreignKey');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "targetName" of HasManyResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], 'sourceName', v, 'relationName', 'foreignKey');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of HasManyResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], 'sourceName', 'targetName', v, 'foreignKey');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "foreignKey" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "foreignKey" of HasManyResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], 'sourceName', 'targetName', 'relationName', v);
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of HasManyResolver.includeTo ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('throws an error if a target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasManyResolver);
+      const promise = R.includeTo(
+        [],
+        'source',
+        'target',
+        'relationName',
+        'foreignKey',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" is not defined',
+      );
+    });
+
+    it('throws an error if a target model does not have datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({name: 'target'});
+      const R = S.get(HasManyResolver);
+      const promise = R.includeTo(
+        [],
+        'source',
+        'target',
+        'relationName',
+        'foreignKey',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have a specified datasource.',
+      );
+    });
+
+    it('does not throw an error if a relation target is not exist', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({});
+      const R = S.get(HasManyResolver);
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [],
+      });
+    });
+
+    it('includes if a primary key is not defined in the source model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({parentId: source[DEF_PK]});
+      const target2 = await targetRep.create({parentId: source[DEF_PK]});
+      const target3 = await targetRep.create({parentId: -1});
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        parentId: source[DEF_PK],
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        parentId: source[DEF_PK],
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        parentId: -1,
+      });
+      const R = S.get(HasManyResolver);
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            id: target1[DEF_PK],
+            parentId: source[DEF_PK],
+          },
+          {
+            id: target2[DEF_PK],
+            parentId: source[DEF_PK],
+          },
+        ],
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({myId: source.myId});
+      const target1 = await targetRep.create({parentId: source.myId});
+      const target2 = await targetRep.create({parentId: source.myId});
+      const target3 = await targetRep.create({parentId: -1});
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        parentId: source.myId,
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        parentId: source.myId,
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        parentId: -1,
+      });
+      const R = S.get(HasManyResolver);
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId');
+      expect(source).to.be.eql({
+        myId: source.myId,
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            parentId: source.myId,
+          },
+          {
+            [DEF_PK]: target2[DEF_PK],
+            parentId: source.myId,
+          },
+        ],
+      });
+    });
+
+    it('includes if the target model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({parentId: source[DEF_PK]});
+      const target2 = await targetRep.create({parentId: source[DEF_PK]});
+      const target3 = await targetRep.create({parentId: -1});
+      expect(target1).to.be.eql({
+        myId: target1.myId,
+        parentId: source[DEF_PK],
+      });
+      expect(target2).to.be.eql({
+        myId: target2.myId,
+        parentId: source[DEF_PK],
+      });
+      expect(target3).to.be.eql({
+        myId: target3.myId,
+        parentId: -1,
+      });
+      const R = S.get(HasManyResolver);
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            myId: target1.myId,
+            parentId: source[DEF_PK],
+          },
+          {
+            myId: target2.myId,
+            parentId: source[DEF_PK],
+          },
+        ],
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      const target3 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      const R = S.get(HasManyResolver);
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId', {
+        where: {featured: false},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            featured: false,
+            parentId: source[DEF_PK],
+          },
+        ],
+      });
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId', {
+        where: {featured: true},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target2[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+          },
+          {
+            [DEF_PK]: target3[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+          },
+        ],
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        foo: 'fooVal1',
+        bar: 'barVal1',
+        parentId: source[DEF_PK],
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        foo: 'fooVal1',
+        bar: 'barVal1',
+        parentId: source[DEF_PK],
+      });
+      const target2 = await targetRep.create({
+        foo: 'fooVal2',
+        bar: 'barVal2',
+        parentId: source[DEF_PK],
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        foo: 'fooVal2',
+        bar: 'barVal2',
+        parentId: source[DEF_PK],
+      });
+      const target3 = await targetRep.create({
+        foo: 'fooVal3',
+        bar: 'barVal3',
+        parentId: -1,
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        foo: 'fooVal3',
+        bar: 'barVal3',
+        parentId: -1,
+      });
+      const R = S.get(HasManyResolver);
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId', {
+        fields: [DEF_PK, 'bar'],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            bar: target1.bar,
+          },
+          {
+            [DEF_PK]: target2[DEF_PK],
+            bar: target2.bar,
+          },
+        ],
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+        relations: {
+          children: {
+            type: RelationType.HAS_MANY,
+            model: 'modelB',
+            foreignKey: 'parentId',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          children: {
+            type: RelationType.HAS_MANY,
+            model: 'modelC',
+            foreignKey: 'parentId',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a = await aRep.create({});
+      const b1 = await bRep.create({parentId: a.id});
+      const b2 = await bRep.create({parentId: a.id});
+      const b3 = await bRep.create({parentId: -1});
+      const c1 = await cRep.create({parentId: b1.id});
+      const c2 = await cRep.create({parentId: b1.id});
+      const c3 = await cRep.create({parentId: b2.id});
+      const c4 = await cRep.create({parentId: b2.id});
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+      });
+      expect(b1).to.be.eql({
+        id: b1.id,
+        source: 'modelB',
+        parentId: a.id,
+      });
+      expect(b2).to.be.eql({
+        id: b2.id,
+        source: 'modelB',
+        parentId: a.id,
+      });
+      expect(b3).to.be.eql({
+        id: b3.id,
+        source: 'modelB',
+        parentId: -1,
+      });
+      expect(c1).to.be.eql({
+        id: c1.id,
+        source: 'modelC',
+        parentId: b1.id,
+      });
+      expect(c2).to.be.eql({
+        id: c2.id,
+        source: 'modelC',
+        parentId: b1.id,
+      });
+      expect(c3).to.be.eql({
+        id: c3.id,
+        source: 'modelC',
+        parentId: b2.id,
+      });
+      expect(c4).to.be.eql({
+        id: c4.id,
+        source: 'modelC',
+        parentId: b2.id,
+      });
+      const R = S.get(HasManyResolver);
+      await R.includeTo([a], 'modelA', 'modelB', 'children', 'parentId', {
+        include: 'children',
+      });
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+        children: [
+          {
+            id: b1.id,
+            source: 'modelB',
+            parentId: a.id,
+            children: [
+              {
+                id: c1.id,
+                source: 'modelC',
+                parentId: b1.id,
+              },
+              {
+                id: c2.id,
+                source: 'modelC',
+                parentId: b1.id,
+              },
+            ],
+          },
+          {
+            id: b2.id,
+            source: 'modelB',
+            parentId: a.id,
+            children: [
+              {
+                id: c3.id,
+                source: 'modelC',
+                parentId: b2.id,
+              },
+              {
+                id: c4.id,
+                source: 'modelC',
+                parentId: b2.id,
+              },
+            ],
+          },
+        ],
+      });
+    });
+
+    it('does not break the "and" operator of the given "where" clause', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      const target3 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      const R = S.get(HasManyResolver);
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId', {
+        where: {and: [{featured: false}]},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            featured: false,
+            parentId: source[DEF_PK],
+          },
+        ],
+      });
+      delete source.children;
+      await R.includeTo([source], 'source', 'target', 'children', 'parentId', {
+        where: {and: [{featured: true}]},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target2[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+          },
+          {
+            [DEF_PK]: target3[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+          },
+        ],
+      });
+    });
+  });
+
+  describe('includePolymorphicTo', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasManyResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          v,
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasManyResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [v],
+          'source',
+          'target',
+          'relationName',
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of HasManyResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          v,
+          'targetName',
+          'relationName',
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "targetName" of HasManyResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          v,
+          'relationName',
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of HasManyResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'targetName',
+          v,
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "foreignKey" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "foreignKey" of HasManyResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          v,
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "discriminator" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "discriminator" of HasManyResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+          v,
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of HasManyResolver.includePolymorphicTo ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+          'discriminator',
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('throws an error if the given target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasManyResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicTo(
+        [entity],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" is not defined',
+      );
+    });
+
+    it('throws an error if the given target model does not have a datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({name: 'target'});
+      const R = S.get(HasManyResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicTo(
+        [entity],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have a specified datasource.',
+      );
+    });
+
+    it('does not throw an error if a relation target is not found', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [],
+      });
+    });
+
+    it('does not include an entity with a not matched discriminator value', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const targetRel = S.getRepository('target');
+      const source = await sourceRel.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target = await targetRel.create({
+        parentId: source[DEF_PK],
+        parentType: 'unknown',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: 'unknown',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [],
+      });
+    });
+
+    it('includes if a primary key is not defined in the source model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        parentId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        parentId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            id: target1[DEF_PK],
+            parentId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+          {
+            id: target2[DEF_PK],
+            parentId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+        ],
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({myId: source.myId});
+      const target1 = await targetRep.create({
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        parentId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        parentId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        myId: source.myId,
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            parentId: source.myId,
+            parentType: target1.parentType,
+          },
+          {
+            [DEF_PK]: target2[DEF_PK],
+            parentId: source.myId,
+            parentType: target2.parentType,
+          },
+        ],
+      });
+    });
+
+    it('includes if the target model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        myId: target1.myId,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        myId: target2.myId,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        parentId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        myId: target3.myId,
+        parentId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            myId: target1.myId,
+            parentId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+          {
+            myId: target2.myId,
+            parentId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+        ],
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+        {where: {featured: false}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            featured: false,
+            parentId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+        ],
+      });
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+        {where: {featured: true}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target2[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+          {
+            [DEF_PK]: target3[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+            parentType: target3.parentType,
+          },
+        ],
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        foo: 'fooVal1',
+        bar: 'barVal1',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        foo: 'fooVal1',
+        bar: 'barVal1',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        foo: 'fooVal2',
+        bar: 'barVal2',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        foo: 'fooVal2',
+        bar: 'barVal2',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        foo: 'fooVal3',
+        bar: 'barVal3',
+        parentId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        foo: 'fooVal3',
+        bar: 'barVal3',
+        parentId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+        {fields: [DEF_PK, 'bar']},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            bar: target1.bar,
+          },
+          {
+            [DEF_PK]: target2[DEF_PK],
+            bar: target2.bar,
+          },
+        ],
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+        relations: {
+          children: {
+            type: RelationType.HAS_MANY,
+            model: 'modelB',
+            polymorphic: true,
+            foreignKey: 'parentId',
+            discriminator: 'parentType',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          children: {
+            type: RelationType.HAS_MANY,
+            model: 'modelC',
+            polymorphic: true,
+            foreignKey: 'parentId',
+            discriminator: 'parentType',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a = await aRep.create({});
+      const b1 = await bRep.create({parentId: a.id, parentType: 'modelA'});
+      const b2 = await bRep.create({parentId: a.id, parentType: 'modelA'});
+      const c1 = await cRep.create({parentId: b1.id, parentType: 'modelB'});
+      const c2 = await cRep.create({parentId: b1.id, parentType: 'modelB'});
+      const c3 = await cRep.create({parentId: b2.id, parentType: 'modelB'});
+      const c4 = await cRep.create({parentId: b2.id, parentType: 'modelB'});
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+      });
+      expect(b1).to.be.eql({
+        id: b1.id,
+        source: 'modelB',
+        parentId: a.id,
+        parentType: 'modelA',
+      });
+      expect(b2).to.be.eql({
+        id: b2.id,
+        source: 'modelB',
+        parentId: a.id,
+        parentType: 'modelA',
+      });
+      expect(c1).to.be.eql({
+        id: c1.id,
+        source: 'modelC',
+        parentId: b1.id,
+        parentType: 'modelB',
+      });
+      expect(c2).to.be.eql({
+        id: c2.id,
+        source: 'modelC',
+        parentId: b1.id,
+        parentType: 'modelB',
+      });
+      expect(c3).to.be.eql({
+        id: c3.id,
+        source: 'modelC',
+        parentId: b2.id,
+        parentType: 'modelB',
+      });
+      expect(c4).to.be.eql({
+        id: c4.id,
+        source: 'modelC',
+        parentId: b2.id,
+        parentType: 'modelB',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [a],
+        'modelA',
+        'modelB',
+        'children',
+        'parentId',
+        'parentType',
+        {include: 'children'},
+      );
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+        children: [
+          {
+            id: b1.id,
+            source: 'modelB',
+            parentId: a.id,
+            parentType: 'modelA',
+            children: [
+              {
+                id: c1.id,
+                source: 'modelC',
+                parentId: b1.id,
+                parentType: 'modelB',
+              },
+              {
+                id: c2.id,
+                source: 'modelC',
+                parentId: b1.id,
+                parentType: 'modelB',
+              },
+            ],
+          },
+          {
+            id: b2.id,
+            source: 'modelB',
+            parentId: a.id,
+            parentType: 'modelA',
+            children: [
+              {
+                id: c3.id,
+                source: 'modelC',
+                parentId: b2.id,
+                parentType: 'modelB',
+              },
+              {
+                id: c4.id,
+                source: 'modelC',
+                parentId: b2.id,
+                parentType: 'modelB',
+              },
+            ],
+          },
+        ],
+      });
+    });
+
+    it('does not break the "and" operator of the given "where" clause', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+        {where: {and: [{featured: false}]}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            featured: false,
+            parentId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+        ],
+      });
+      delete source.children;
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parentId',
+        'parentType',
+        {where: {and: [{featured: true}]}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target2[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+          {
+            [DEF_PK]: target3[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+            parentType: target3.parentType,
+          },
+        ],
+      });
+    });
+  });
+
+  describe('includePolymorphicByRelationName', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasManyResolver.includePolymorphicByRelationName requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          v,
+          'sourceName',
+          'targetName',
+          'relationName',
+          'targetRelationName',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({
+        name: 'target',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasManyResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [v],
+          'source',
+          'target',
+          'children',
+          'parent',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of HasManyResolver.includePolymorphicByRelationName requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          v,
+          'targetName',
+          'relationName',
+          'targetRelationName',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "targetName" of HasManyResolver.includePolymorphicByRelationName requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          'sourceName',
+          v,
+          'relationName',
+          'targetRelationName',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of HasManyResolver.includePolymorphicByRelationName requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          'sourceName',
+          'targetName',
+          v,
+          'targetRelationName',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetRelationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The parameter "targetRelationName" of HasManyResolver.includePolymorphicByRelationName requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          v,
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(HasManyResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of HasManyResolver.includePolymorphicByRelationName ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          'targetRelationName',
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('throws an error if the given target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasManyResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" is not defined',
+      );
+    });
+
+    it('throws an error if the given target model does not have the given relation name', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({name: 'target'});
+      const R = S.get(HasManyResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have relation name "parent".',
+      );
+    });
+
+    it('throws an error if the target relation is not "belongsTo"', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({
+        name: 'target',
+        relations: {
+          parent: {
+            type: RelationType.REFERENCES_MANY,
+            model: 'source',
+          },
+        },
+      });
+      const R = S.get(HasManyResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The relation "children" of the model "source" is a polymorphic "hasMany" relation, ' +
+          'so it requires the target relation "parent" to be a polymorphic "belongsTo", ' +
+          'but "referencesMany" type given.',
+      );
+    });
+
+    it('throws an error if the target relation is not polymorphic', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({
+        name: 'target',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            model: 'source',
+          },
+        },
+      });
+      const R = S.get(HasManyResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The relation "children" of the model "source" is a polymorphic ' +
+          '"hasMany" relation, so it requires the target relation "parent" ' +
+          'to be a polymorphic too.',
+      );
+    });
+
+    it('throws an error if the given target model does not have a datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({
+        name: 'target',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const R = S.get(HasManyResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have a specified datasource.',
+      );
+    });
+
+    it('does not throw an error if a relation target is not found', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [],
+      });
+    });
+
+    it('does not include an entity with a not matched discriminator value', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRel = S.getRepository('source');
+      const targetRel = S.getRepository('target');
+      const source = await sourceRel.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target = await targetRel.create({
+        parentId: source[DEF_PK],
+        parentType: 'unknown',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: 'unknown',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [],
+      });
+    });
+
+    it('includes if a primary key is not defined in the source model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        parentId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        parentId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            id: target1[DEF_PK],
+            parentId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+          {
+            id: target2[DEF_PK],
+            parentId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+        ],
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({myId: source.myId});
+      const target1 = await targetRep.create({
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        parentId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        parentId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        myId: source.myId,
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            parentId: source.myId,
+            parentType: target1.parentType,
+          },
+          {
+            [DEF_PK]: target2[DEF_PK],
+            parentId: source.myId,
+            parentType: target2.parentType,
+          },
+        ],
+      });
+    });
+
+    it('includes if the target model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        myId: target1.myId,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        myId: target2.myId,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        parentId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        myId: target3.myId,
+        parentId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            myId: target1.myId,
+            parentId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+          {
+            myId: target2.myId,
+            parentId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+        ],
+      });
+    });
+
+    it('includes if the target model has a custom "foreignKey"', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+            foreignKey: 'relationId',
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        relationId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        myId: target1.myId,
+        relationId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        relationId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        myId: target2.myId,
+        relationId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        relationId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        myId: target3.myId,
+        relationId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            myId: target1.myId,
+            relationId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+          {
+            myId: target2.myId,
+            relationId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+        ],
+      });
+    });
+
+    it('includes if the target model has a custom "discriminator"', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+            discriminator: 'relationType',
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        parentId: source[DEF_PK],
+        relationType: 'source',
+      });
+      expect(target1).to.be.eql({
+        myId: target1.myId,
+        parentId: source[DEF_PK],
+        relationType: 'source',
+      });
+      const target2 = await targetRep.create({
+        parentId: source[DEF_PK],
+        relationType: 'source',
+      });
+      expect(target2).to.be.eql({
+        myId: target2.myId,
+        parentId: source[DEF_PK],
+        relationType: 'source',
+      });
+      const target3 = await targetRep.create({
+        parentId: -1,
+        relationType: 'source',
+      });
+      expect(target3).to.be.eql({
+        myId: target3.myId,
+        parentId: -1,
+        relationType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            myId: target1.myId,
+            parentId: source[DEF_PK],
+            relationType: target1.relationType,
+          },
+          {
+            myId: target2.myId,
+            parentId: source[DEF_PK],
+            relationType: target2.relationType,
+          },
+        ],
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+        {where: {featured: false}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            featured: false,
+            parentId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+        ],
+      });
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+        {where: {featured: true}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target2[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+          {
+            [DEF_PK]: target3[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+            parentType: target3.parentType,
+          },
+        ],
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        foo: 'fooVal1',
+        bar: 'barVal1',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        foo: 'fooVal1',
+        bar: 'barVal1',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        foo: 'fooVal2',
+        bar: 'barVal2',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        foo: 'fooVal2',
+        bar: 'barVal2',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        foo: 'fooVal3',
+        bar: 'barVal3',
+        parentId: -1,
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        foo: 'fooVal3',
+        bar: 'barVal3',
+        parentId: -1,
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+        {fields: [DEF_PK, 'bar']},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            bar: target1.bar,
+          },
+          {
+            [DEF_PK]: target2[DEF_PK],
+            bar: target2.bar,
+          },
+        ],
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+        relations: {
+          children: {
+            type: RelationType.HAS_MANY,
+            model: 'modelB',
+            polymorphic: 'parent',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+          children: {
+            type: RelationType.HAS_MANY,
+            model: 'modelC',
+            polymorphic: 'parent',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a = await aRep.create({});
+      const b1 = await bRep.create({parentId: a.id, parentType: 'modelA'});
+      const b2 = await bRep.create({parentId: a.id, parentType: 'modelA'});
+      const c1 = await cRep.create({parentId: b1.id, parentType: 'modelB'});
+      const c2 = await cRep.create({parentId: b1.id, parentType: 'modelB'});
+      const c3 = await cRep.create({parentId: b2.id, parentType: 'modelB'});
+      const c4 = await cRep.create({parentId: b2.id, parentType: 'modelB'});
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+      });
+      expect(b1).to.be.eql({
+        id: b1.id,
+        source: 'modelB',
+        parentId: a.id,
+        parentType: 'modelA',
+      });
+      expect(b2).to.be.eql({
+        id: b2.id,
+        source: 'modelB',
+        parentId: a.id,
+        parentType: 'modelA',
+      });
+      expect(c1).to.be.eql({
+        id: c1.id,
+        source: 'modelC',
+        parentId: b1.id,
+        parentType: 'modelB',
+      });
+      expect(c2).to.be.eql({
+        id: c2.id,
+        source: 'modelC',
+        parentId: b1.id,
+        parentType: 'modelB',
+      });
+      expect(c3).to.be.eql({
+        id: c3.id,
+        source: 'modelC',
+        parentId: b2.id,
+        parentType: 'modelB',
+      });
+      expect(c4).to.be.eql({
+        id: c4.id,
+        source: 'modelC',
+        parentId: b2.id,
+        parentType: 'modelB',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [a],
+        'modelA',
+        'modelB',
+        'children',
+        'parent',
+        {include: 'children'},
+      );
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+        children: [
+          {
+            id: b1.id,
+            source: 'modelB',
+            parentId: a.id,
+            parentType: 'modelA',
+            children: [
+              {
+                id: c1.id,
+                source: 'modelC',
+                parentId: b1.id,
+                parentType: 'modelB',
+              },
+              {
+                id: c2.id,
+                source: 'modelC',
+                parentId: b1.id,
+                parentType: 'modelB',
+              },
+            ],
+          },
+          {
+            id: b2.id,
+            source: 'modelB',
+            parentId: a.id,
+            parentType: 'modelA',
+            children: [
+              {
+                id: c3.id,
+                source: 'modelC',
+                parentId: b2.id,
+                parentType: 'modelB',
+              },
+              {
+                id: c4.id,
+                source: 'modelC',
+                parentId: b2.id,
+                parentType: 'modelB',
+              },
+            ],
+          },
+        ],
+      });
+    });
+
+    it('does not break the "and" operator of the given "where" clause', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const target3 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      const R = S.get(HasManyResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+        {where: {and: [{featured: false}]}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            featured: false,
+            parentId: source[DEF_PK],
+            parentType: target1.parentType,
+          },
+        ],
+      });
+      delete source.children;
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'children',
+        'parent',
+        {where: {and: [{featured: true}]}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        children: [
+          {
+            [DEF_PK]: target2[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+            parentType: target2.parentType,
+          },
+          {
+            [DEF_PK]: target3[DEF_PK],
+            featured: true,
+            parentId: source[DEF_PK],
+            parentType: target3.parentType,
+          },
+        ],
+      });
+    });
+  });
+});

+ 307 - 0
src/relations/has-one-resolver.js

@@ -0,0 +1,307 @@
+import {Service} from '../service/index.js';
+import {cloneDeep} from '../utils/index.js';
+import {RelationType} from '../definition/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {RepositoryRegistry} from '../repository/index.js';
+import {ModelDefinitionUtils} from '../definition/index.js';
+
+/**
+ * Has one resolver.
+ */
+export class HasOneResolver extends Service {
+  /**
+   * Include to.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} targetName
+   * @param {string} relationName
+   * @param {string} foreignKey
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includeTo(
+    entities,
+    sourceName,
+    targetName,
+    relationName,
+    foreignKey,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of HasOneResolver.includeTo requires ' +
+          'an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of HasOneResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!targetName || typeof targetName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetName" of HasOneResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        targetName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of HasOneResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        relationName,
+      );
+    if (!foreignKey || typeof foreignKey !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "foreignKey" of HasOneResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        foreignKey,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of HasOneResolver.includeTo ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+
+    const sourcePkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(sourceName);
+    const sourceIds = [];
+    entities.forEach(entity => {
+      if (!entity || typeof entity !== 'object' || Array.isArray(entity))
+        throw new InvalidArgumentError(
+          'The parameter "entities" of HasOneResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          entity,
+        );
+      const sourceId = entity[sourcePkPropName];
+      if (sourceIds.includes(sourceId)) return;
+      sourceIds.push(sourceId);
+    });
+
+    const promises = [];
+    const targetRepository =
+      this.get(RepositoryRegistry).getRepository(targetName);
+    scope = scope ? cloneDeep(scope) : {};
+    const targetBySourceId = new Map();
+    sourceIds.forEach(sourceId => {
+      const filter = cloneDeep(scope);
+      filter.where = {
+        and: [{[foreignKey]: sourceId}, ...(scope.where ? [scope.where] : [])],
+      };
+      filter.limit = 1;
+      promises.push(
+        targetRepository.find(filter).then(result => {
+          if (result.length) targetBySourceId.set(sourceId, result[0]);
+        }),
+      );
+    });
+    await Promise.all(promises);
+
+    Array.from(targetBySourceId.keys()).forEach(sourceId => {
+      const sources = entities.filter(v => v[sourcePkPropName] === sourceId);
+      sources.forEach(v => (v[relationName] = targetBySourceId.get(sourceId)));
+    });
+  }
+
+  /**
+   * Include polymorphic to.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} targetName
+   * @param {string} relationName
+   * @param {string} foreignKey
+   * @param {string} discriminator
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includePolymorphicTo(
+    entities,
+    sourceName,
+    targetName,
+    relationName,
+    foreignKey,
+    discriminator,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of HasOneResolver.includePolymorphicTo requires ' +
+          'an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of HasOneResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!targetName || typeof targetName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetName" of HasOneResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        targetName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of HasOneResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        relationName,
+      );
+    if (!foreignKey || typeof foreignKey !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "foreignKey" of HasOneResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        foreignKey,
+      );
+    if (!discriminator || typeof discriminator !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "discriminator" of HasOneResolver.includePolymorphicTo requires ' +
+          'a non-empty String, but %s given.',
+        discriminator,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of HasOneResolver.includePolymorphicTo ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+
+    const sourcePkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(sourceName);
+    const sourceIds = [];
+    entities.forEach(entity => {
+      if (!entity || typeof entity !== 'object' || Array.isArray(entity))
+        throw new InvalidArgumentError(
+          'The parameter "entities" of HasOneResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          entity,
+        );
+      const sourceId = entity[sourcePkPropName];
+      if (sourceIds.includes(sourceId)) return;
+      sourceIds.push(sourceId);
+    });
+
+    const promises = [];
+    const targetRepository =
+      this.get(RepositoryRegistry).getRepository(targetName);
+    scope = scope ? cloneDeep(scope) : {};
+    const targetBySourceId = new Map();
+    sourceIds.forEach(sourceId => {
+      const filter = cloneDeep(scope);
+      filter.where = {
+        and: [
+          {[foreignKey]: sourceId, [discriminator]: sourceName},
+          ...(scope.where ? [scope.where] : []),
+        ],
+      };
+      filter.limit = 1;
+      promises.push(
+        targetRepository.find(filter).then(result => {
+          if (result.length) targetBySourceId.set(sourceId, result[0]);
+        }),
+      );
+    });
+    await Promise.all(promises);
+
+    Array.from(targetBySourceId.keys()).forEach(sourceId => {
+      const sources = entities.filter(v => v[sourcePkPropName] === sourceId);
+      sources.forEach(v => (v[relationName] = targetBySourceId.get(sourceId)));
+    });
+  }
+
+  /**
+   * Include polymorphic by relation name.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} targetName
+   * @param {string} relationName
+   * @param {string} targetRelationName
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includePolymorphicByRelationName(
+    entities,
+    sourceName,
+    targetName,
+    relationName,
+    targetRelationName,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of HasOneResolver.includePolymorphicByRelationName requires ' +
+          'an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of HasOneResolver.includePolymorphicByRelationName requires ' +
+          'a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!targetName || typeof targetName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetName" of HasOneResolver.includePolymorphicByRelationName requires ' +
+          'a non-empty String, but %s given.',
+        targetName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of HasOneResolver.includePolymorphicByRelationName requires ' +
+          'a non-empty String, but %s given.',
+        relationName,
+      );
+    if (!targetRelationName || typeof targetRelationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetRelationName" of HasOneResolver.includePolymorphicByRelationName requires ' +
+          'a non-empty String, but %s given.',
+        targetRelationName,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of HasOneResolver.includePolymorphicByRelationName ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+
+    const targetRelationDef = this.get(
+      ModelDefinitionUtils,
+    ).getRelationDefinitionByName(targetName, targetRelationName);
+    if (targetRelationDef.type !== RelationType.BELONGS_TO)
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s is a polymorphic "hasOne" relation, ' +
+          'so it requires the target relation %s to be a polymorphic "belongsTo", ' +
+          'but %s type given.',
+        relationName,
+        sourceName,
+        targetRelationName,
+        targetRelationDef.type,
+      );
+    if (!targetRelationDef.polymorphic)
+      throw new InvalidArgumentError(
+        'The relation %s of the model %s is a polymorphic "hasOne" relation, ' +
+          'so it requires the target relation %s to be a polymorphic too.',
+        relationName,
+        sourceName,
+        targetRelationName,
+      );
+    const foreignKey =
+      targetRelationDef.foreignKey || `${targetRelationName}Id`;
+    const discriminator =
+      targetRelationDef.discriminator || `${targetRelationName}Type`;
+
+    return this.includePolymorphicTo(
+      entities,
+      sourceName,
+      targetName,
+      relationName,
+      foreignKey,
+      discriminator,
+      scope,
+    );
+  }
+}

+ 2274 - 0
src/relations/has-one-resolver.spec.js

@@ -0,0 +1,2274 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {Schema} from '../schema.js';
+import {DataType} from '../definition/index.js';
+import {RelationType} from '../definition/index.js';
+import {HasOneResolver} from './has-one-resolver.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../definition/index.js';
+
+describe('HasOneResolver', function () {
+  describe('includeTo', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasOneResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo(
+          v,
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasOneResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([v], 'source', 'target', 'relationName', 'foreignKey');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of HasOneResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], v, 'targetName', 'relationName', 'foreignKey');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "targetName" of HasOneResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], 'sourceName', v, 'relationName', 'foreignKey');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of HasOneResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], 'sourceName', 'targetName', v, 'foreignKey');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "foreignKey" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "foreignKey" of HasOneResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], 'sourceName', 'targetName', 'relationName', v);
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of HasOneResolver.includeTo ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('throws an error if a target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasOneResolver);
+      const promise = R.includeTo(
+        [],
+        'source',
+        'target',
+        'relationName',
+        'foreignKey',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" is not defined',
+      );
+    });
+
+    it('throws an error if a target model does not have datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({name: 'target'});
+      const R = S.get(HasOneResolver);
+      const promise = R.includeTo(
+        [],
+        'source',
+        'target',
+        'relationName',
+        'foreignKey',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have a specified datasource.',
+      );
+    });
+
+    it('does not throw an error if a relation target is not exist', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({});
+      const R = S.get(HasOneResolver);
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+    });
+
+    it('includes if a primary key is not defined in the source model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target = await targetRep.create({parentId: source[DEF_PK]});
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source[DEF_PK],
+      });
+      const R = S.get(HasOneResolver);
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          id: target[DEF_PK],
+          parentId: source[DEF_PK],
+        },
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({myId: source.myId});
+      const target = await targetRep.create({parentId: source.myId});
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source.myId,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId');
+      expect(source).to.be.eql({
+        myId: source.myId,
+        child: {
+          [DEF_PK]: target[DEF_PK],
+          parentId: source.myId,
+        },
+      });
+    });
+
+    it('includes if the target model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target = await targetRep.create({parentId: source[DEF_PK]});
+      expect(target).to.be.eql({
+        myId: target.myId,
+        parentId: source[DEF_PK],
+      });
+      const R = S.get(HasOneResolver);
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          myId: target.myId,
+          parentId: source[DEF_PK],
+        },
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      const R = S.get(HasOneResolver);
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId', {
+        where: {featured: false},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target1[DEF_PK],
+          featured: false,
+          parentId: source[DEF_PK],
+        },
+      });
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId', {
+        where: {featured: true},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target2[DEF_PK],
+          featured: true,
+          parentId: source[DEF_PK],
+        },
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target = await targetRep.create({
+        foo: 'fooVal',
+        bar: 'barVal',
+        parentId: source[DEF_PK],
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        foo: target.foo,
+        bar: target.bar,
+        parentId: source[DEF_PK],
+      });
+      const R = S.get(HasOneResolver);
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId', {
+        fields: [DEF_PK, 'bar'],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target[DEF_PK],
+          bar: target.bar,
+        },
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+        relations: {
+          child: {
+            type: RelationType.HAS_ONE,
+            model: 'modelB',
+            foreignKey: 'parentId',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          child: {
+            type: RelationType.HAS_ONE,
+            model: 'modelC',
+            foreignKey: 'parentId',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a = await aRep.create({});
+      const b = await bRep.create({parentId: a.id});
+      const c = await cRep.create({parentId: b.id});
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+      });
+      expect(b).to.be.eql({
+        id: b.id,
+        source: 'modelB',
+        parentId: a.id,
+      });
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentId: b.id,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includeTo([a], 'modelA', 'modelB', 'child', 'parentId', {
+        include: 'child',
+      });
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+        child: {
+          id: b.id,
+          source: 'modelB',
+          parentId: a.id,
+          child: {
+            id: c.id,
+            source: 'modelC',
+            parentId: b.id,
+          },
+        },
+      });
+    });
+
+    it('does not break the "and" operator of the given "where" clause', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+      });
+      const R = S.get(HasOneResolver);
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId', {
+        where: {and: [{featured: false}]},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target1[DEF_PK],
+          featured: false,
+          parentId: source[DEF_PK],
+        },
+      });
+      delete source.child;
+      await R.includeTo([source], 'source', 'target', 'child', 'parentId', {
+        where: {and: [{featured: true}]},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target2[DEF_PK],
+          featured: true,
+          parentId: source[DEF_PK],
+        },
+      });
+    });
+  });
+
+  describe('includePolymorphicTo', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasOneResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          v,
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasOneResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [v],
+          'source',
+          'target',
+          'relationName',
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of HasOneResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          v,
+          'targetName',
+          'relationName',
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "targetName" of HasOneResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          v,
+          'relationName',
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of HasOneResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'targetName',
+          v,
+          'foreignKey',
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "foreignKey" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "foreignKey" of HasOneResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          v,
+          'discriminator',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "discriminator" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "discriminator" of HasOneResolver.includePolymorphicTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+          v,
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of HasOneResolver.includePolymorphicTo ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          'foreignKey',
+          'discriminator',
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('throws an error if the given target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasOneResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicTo(
+        [entity],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" is not defined',
+      );
+    });
+
+    it('throws an error if the given target model does not have a datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({name: 'target'});
+      const R = S.get(HasOneResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicTo(
+        [entity],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have a specified datasource.',
+      );
+    });
+
+    it('does not throw an error if a relation target is not found', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+    });
+
+    it('does not include an entity with a not matched discriminator value', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const targetRel = S.getRepository('target');
+      const source = await sourceRel.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target = await targetRel.create({
+        parentId: source[DEF_PK],
+        parentType: 'unknown',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: 'unknown',
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+    });
+
+    it('includes if a primary key is not defined in the source model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          id: target[DEF_PK],
+          parentId: source[DEF_PK],
+          parentType: target.parentType,
+        },
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({myId: source.myId});
+      const target = await targetRep.create({
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source.myId,
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        myId: source.myId,
+        child: {
+          [DEF_PK]: target[DEF_PK],
+          parentId: source.myId,
+          parentType: target.parentType,
+        },
+      });
+    });
+
+    it('includes if the target model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        myId: target.myId,
+        parentId: source[DEF_PK],
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          myId: target.myId,
+          parentId: source[DEF_PK],
+          parentType: target.parentType,
+        },
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: target1.parentType,
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: target2.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+        {where: {featured: false}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target1[DEF_PK],
+          featured: false,
+          parentId: source[DEF_PK],
+          parentType: target1.parentType,
+        },
+      });
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+        {where: {featured: true}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target2[DEF_PK],
+          featured: true,
+          parentId: source[DEF_PK],
+          parentType: target2.parentType,
+        },
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target = await targetRep.create({
+        foo: 'fooVal',
+        bar: 'barVal',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        foo: target.foo,
+        bar: target.bar,
+        parentId: source[DEF_PK],
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+        {fields: [DEF_PK, 'bar']},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target[DEF_PK],
+          bar: target.bar,
+        },
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+        relations: {
+          child: {
+            type: RelationType.HAS_ONE,
+            model: 'modelB',
+            polymorphic: true,
+            foreignKey: 'parentId',
+            discriminator: 'parentType',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          child: {
+            type: RelationType.HAS_ONE,
+            model: 'modelC',
+            polymorphic: true,
+            foreignKey: 'parentId',
+            discriminator: 'parentType',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a = await aRep.create({});
+      const b = await bRep.create({parentId: a.id, parentType: 'modelA'});
+      const c = await cRep.create({parentId: b.id, parentType: 'modelB'});
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+      });
+      expect(b).to.be.eql({
+        id: b.id,
+        source: 'modelB',
+        parentId: a.id,
+        parentType: 'modelA',
+      });
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentId: b.id,
+        parentType: 'modelB',
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [a],
+        'modelA',
+        'modelB',
+        'child',
+        'parentId',
+        'parentType',
+        {include: 'child'},
+      );
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+        child: {
+          id: b.id,
+          source: 'modelB',
+          parentId: a.id,
+          parentType: 'modelA',
+          child: {
+            id: c.id,
+            source: 'modelC',
+            parentId: b.id,
+            parentType: 'modelB',
+          },
+        },
+      });
+    });
+
+    it('does not break the "and" operator of the given "where" clause', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: target1.parentType,
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: target2.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+        {where: {and: [{featured: false}]}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target1[DEF_PK],
+          featured: false,
+          parentId: source[DEF_PK],
+          parentType: target1.parentType,
+        },
+      });
+      delete source.child;
+      await R.includePolymorphicTo(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parentId',
+        'parentType',
+        {where: {and: [{featured: true}]}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target2[DEF_PK],
+          featured: true,
+          parentId: source[DEF_PK],
+          parentType: target2.parentType,
+        },
+      });
+    });
+  });
+
+  describe('includePolymorphicByRelationName', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasOneResolver.includePolymorphicByRelationName requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          v,
+          'sourceName',
+          'targetName',
+          'relationName',
+          'targetRelationName',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({
+        name: 'target',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of HasOneResolver.includePolymorphicTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [v],
+          'source',
+          'target',
+          'child',
+          'parent',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of HasOneResolver.includePolymorphicByRelationName requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          v,
+          'targetName',
+          'relationName',
+          'targetRelationName',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "targetName" of HasOneResolver.includePolymorphicByRelationName requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          'sourceName',
+          v,
+          'relationName',
+          'targetRelationName',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of HasOneResolver.includePolymorphicByRelationName requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          'sourceName',
+          'targetName',
+          v,
+          'targetRelationName',
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetRelationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The parameter "targetRelationName" of HasOneResolver.includePolymorphicByRelationName requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          v,
+        );
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(HasOneResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of HasOneResolver.includePolymorphicByRelationName ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includePolymorphicByRelationName(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          'targetRelationName',
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('throws an error if the given target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(HasOneResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" is not defined',
+      );
+    });
+
+    it('throws an error if the given target model does not have the given relation name', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({name: 'target'});
+      const R = S.get(HasOneResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have relation name "parent".',
+      );
+    });
+
+    it('throws an error if the target relation is not "belongsTo"', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({
+        name: 'target',
+        relations: {
+          parent: {
+            type: RelationType.REFERENCES_MANY,
+            model: 'source',
+          },
+        },
+      });
+      const R = S.get(HasOneResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The relation "child" of the model "source" is a polymorphic "hasOne" relation, ' +
+          'so it requires the target relation "parent" to be a polymorphic "belongsTo", ' +
+          'but "referencesMany" type given.',
+      );
+    });
+
+    it('throws an error if the target relation is not polymorphic', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({
+        name: 'target',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            model: 'source',
+          },
+        },
+      });
+      const R = S.get(HasOneResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The relation "child" of the model "source" is a polymorphic ' +
+          '"hasOne" relation, so it requires the target relation "parent" ' +
+          'to be a polymorphic too.',
+      );
+    });
+
+    it('throws an error if the given target model does not have a datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      S.defineModel({
+        name: 'target',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const R = S.get(HasOneResolver);
+      const entity = {[DEF_PK]: 1};
+      const promise = R.includePolymorphicByRelationName(
+        [entity],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have a specified datasource.',
+      );
+    });
+
+    it('does not throw an error if a relation target is not found', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+    });
+
+    it('does not include an entity with a not matched discriminator value', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRel = S.getRepository('source');
+      const targetRel = S.getRepository('target');
+      const source = await sourceRel.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target = await targetRel.create({
+        parentId: source[DEF_PK],
+        parentType: 'unknown',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: 'unknown',
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+    });
+
+    it('includes if a primary key is not defined in the source model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source[DEF_PK],
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          id: target[DEF_PK],
+          parentId: source[DEF_PK],
+          parentType: target.parentType,
+        },
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({myId: source.myId});
+      const target = await targetRep.create({
+        parentId: source.myId,
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        parentId: source.myId,
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        myId: source.myId,
+        child: {
+          [DEF_PK]: target[DEF_PK],
+          parentId: source.myId,
+          parentType: target.parentType,
+        },
+      });
+    });
+
+    it('includes if the target model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target = await targetRep.create({
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        myId: target.myId,
+        parentId: source[DEF_PK],
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          myId: target.myId,
+          parentId: source[DEF_PK],
+          parentType: target.parentType,
+        },
+      });
+    });
+
+    it('includes if the target model has a custom "foreignKey"', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+            foreignKey: 'relationId',
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target = await targetRep.create({
+        relationId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        myId: target.myId,
+        relationId: source[DEF_PK],
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          myId: target.myId,
+          relationId: source[DEF_PK],
+          parentType: target.parentType,
+        },
+      });
+    });
+
+    it('includes if the target model has a custom "discriminator"', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+            discriminator: 'relationType',
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target = await targetRep.create({
+        parentId: source[DEF_PK],
+        relationType: 'source',
+      });
+      expect(target).to.be.eql({
+        myId: target.myId,
+        parentId: source[DEF_PK],
+        relationType: target.relationType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          myId: target.myId,
+          parentId: source[DEF_PK],
+          relationType: target.relationType,
+        },
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({[DEF_PK]: source[DEF_PK]});
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: target1.parentType,
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: target2.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+        {where: {featured: false}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target1[DEF_PK],
+          featured: false,
+          parentId: source[DEF_PK],
+          parentType: target1.parentType,
+        },
+      });
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+        {where: {featured: true}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target2[DEF_PK],
+          featured: true,
+          parentId: source[DEF_PK],
+          parentType: target2.parentType,
+        },
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target = await targetRep.create({
+        foo: 'fooVal',
+        bar: 'barVal',
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target).to.be.eql({
+        [DEF_PK]: target[DEF_PK],
+        foo: target.foo,
+        bar: target.bar,
+        parentId: source[DEF_PK],
+        parentType: target.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+        {fields: [DEF_PK, 'bar']},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target[DEF_PK],
+          bar: target.bar,
+        },
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+        relations: {
+          child: {
+            type: RelationType.HAS_ONE,
+            model: 'modelB',
+            polymorphic: 'parent',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+          child: {
+            type: RelationType.HAS_ONE,
+            model: 'modelC',
+            polymorphic: 'parent',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a = await aRep.create({});
+      const b = await bRep.create({parentId: a.id, parentType: 'modelA'});
+      const c = await cRep.create({parentId: b.id, parentType: 'modelB'});
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+      });
+      expect(b).to.be.eql({
+        id: b.id,
+        source: 'modelB',
+        parentId: a.id,
+        parentType: 'modelA',
+      });
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentId: b.id,
+        parentType: 'modelB',
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [a],
+        'modelA',
+        'modelB',
+        'child',
+        'parent',
+        {include: 'child'},
+      );
+      expect(a).to.be.eql({
+        id: a.id,
+        source: 'modelA',
+        child: {
+          id: b.id,
+          source: 'modelB',
+          parentId: a.id,
+          parentType: 'modelA',
+          child: {
+            id: c.id,
+            source: 'modelC',
+            parentId: b.id,
+            parentType: 'modelB',
+          },
+        },
+      });
+    });
+
+    it('does not break the "and" operator of the given "where" clause', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            polymorphic: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const source = await sourceRep.create({});
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+      });
+      const target1 = await targetRep.create({
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        featured: false,
+        parentId: source[DEF_PK],
+        parentType: target1.parentType,
+      });
+      const target2 = await targetRep.create({
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: 'source',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        featured: true,
+        parentId: source[DEF_PK],
+        parentType: target2.parentType,
+      });
+      const R = S.get(HasOneResolver);
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+        {where: {and: [{featured: false}]}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target1[DEF_PK],
+          featured: false,
+          parentId: source[DEF_PK],
+          parentType: target1.parentType,
+        },
+      });
+      delete source.child;
+      await R.includePolymorphicByRelationName(
+        [source],
+        'source',
+        'target',
+        'child',
+        'parent',
+        {where: {and: [{featured: true}]}},
+      );
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        child: {
+          [DEF_PK]: target2[DEF_PK],
+          featured: true,
+          parentId: source[DEF_PK],
+          parentType: target2.parentType,
+        },
+      });
+    });
+  });
+});

+ 4 - 0
src/relations/index.js

@@ -0,0 +1,4 @@
+export * from './has-one-resolver.js';
+export * from './has-many-resolver.js';
+export * from './belongs-to-resolver.js';
+export * from './references-many-resolver.js';

+ 111 - 0
src/relations/references-many-resolver.js

@@ -0,0 +1,111 @@
+import {Service} from '../service/index.js';
+import {cloneDeep} from '../utils/index.js';
+import {singularize} from '../utils/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {RepositoryRegistry} from '../repository/index.js';
+import {ModelDefinitionUtils} from '../definition/index.js';
+
+/**
+ * References many resolver.
+ */
+export class ReferencesManyResolver extends Service {
+  /**
+   * Include to.
+   *
+   * @param {Record<string, unknown>[]} entities
+   * @param {string} sourceName
+   * @param {string} targetName
+   * @param {string} relationName
+   * @param {string|undefined} foreignKey
+   * @param {Record<string, unknown>|undefined} scope
+   * @return {Promise<void>}
+   */
+  async includeTo(
+    entities,
+    sourceName,
+    targetName,
+    relationName,
+    foreignKey = undefined,
+    scope = undefined,
+  ) {
+    if (!entities || !Array.isArray(entities))
+      throw new InvalidArgumentError(
+        'The parameter "entities" of ReferencesManyResolver.includeTo requires ' +
+          'an Array of Object, but %s given.',
+        entities,
+      );
+    if (!sourceName || typeof sourceName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "sourceName" of ReferencesManyResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        sourceName,
+      );
+    if (!targetName || typeof targetName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "targetName" of ReferencesManyResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        targetName,
+      );
+    if (!relationName || typeof relationName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "relationName" of ReferencesManyResolver.includeTo requires ' +
+          'a non-empty String, but %s given.',
+        relationName,
+      );
+    if (foreignKey && typeof foreignKey !== 'string')
+      throw new InvalidArgumentError(
+        'The provided parameter "foreignKey" of ReferencesManyResolver.includeTo ' +
+          'should be a String, but %s given.',
+        foreignKey,
+      );
+    if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
+      throw new InvalidArgumentError(
+        'The provided parameter "scope" of ReferencesManyResolver.includeTo ' +
+          'should be an Object, but %s given.',
+        scope,
+      );
+    if (foreignKey == null) {
+      const singularRelationName = singularize(relationName);
+      foreignKey = `${singularRelationName}Ids`;
+    }
+    const targetIds = entities.reduce((acc, entity) => {
+      if (!entity || typeof entity !== 'object' || Array.isArray(entity))
+        throw new InvalidArgumentError(
+          'The parameter "entities" of ReferencesManyResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          entity,
+        );
+      const ids = entity[foreignKey];
+      if (Array.isArray(ids))
+        ids.forEach(id => {
+          if (id == null || acc.includes(id)) return;
+          acc.push(id);
+        });
+      return acc;
+    }, []);
+
+    const targetRepository =
+      this.get(RepositoryRegistry).getRepository(targetName);
+    const targetPkPropName =
+      this.get(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(targetName);
+    scope = scope ? cloneDeep(scope) : {};
+    const filter = cloneDeep(scope);
+    filter.where = {
+      and: [
+        {[targetPkPropName]: {inq: targetIds}},
+        ...(scope.where ? [scope.where] : []),
+      ],
+    };
+    const targets = await targetRepository.find(filter);
+
+    entities.forEach(entity => {
+      const ids = entity[foreignKey];
+      entity[relationName] = [];
+      if (Array.isArray(ids))
+        targets.forEach(target => {
+          const targetId = target[targetPkPropName];
+          if (ids.includes(targetId)) entity[relationName].push(target);
+        });
+    });
+  }
+}

+ 631 - 0
src/relations/references-many-resolver.spec.js

@@ -0,0 +1,631 @@
+import {format} from 'util';
+import {expect} from 'chai';
+import {Schema} from '../schema.js';
+import {DataType} from '../definition/index.js';
+import {RelationType} from '../definition/index.js';
+import {ReferencesManyResolver} from './references-many-resolver.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../definition/index.js';
+
+describe('ReferencesManyResolver', function () {
+  describe('includeTo', function () {
+    it('requires the "entities" parameter to be an array', async function () {
+      const S = new Schema();
+      const R = S.get(ReferencesManyResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of ReferencesManyResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo(v, 'sourceName', 'targetName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires elements of the "entities" parameter to be an Object', async function () {
+      const S = new Schema();
+      const R = S.get(ReferencesManyResolver);
+      const error = v =>
+        format(
+          'The parameter "entities" of ReferencesManyResolver.includeTo requires ' +
+            'an Array of Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([v], 'sourceName', 'targetName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "sourceName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(ReferencesManyResolver);
+      const error = v =>
+        format(
+          'The parameter "sourceName" of ReferencesManyResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v => R.includeTo([], v, 'targetName', 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "targetName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(ReferencesManyResolver);
+      const error = v =>
+        format(
+          'The parameter "targetName" of ReferencesManyResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v => R.includeTo([], 'sourceName', v, 'relationName');
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the "relationName" parameter to be a non-empty string', async function () {
+      const S = new Schema();
+      const R = S.get(ReferencesManyResolver);
+      const error = v =>
+        format(
+          'The parameter "relationName" of ReferencesManyResolver.includeTo requires ' +
+            'a non-empty String, but %s given.',
+          v,
+        );
+      const throwable = v => R.includeTo([], 'sourceName', 'targetName', v);
+      await expect(throwable('')).to.be.rejectedWith(error('""'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable(false)).to.be.rejectedWith(error('false'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+      await expect(throwable(undefined)).to.be.rejectedWith(error('undefined'));
+      await expect(throwable(null)).to.be.rejectedWith(error('null'));
+    });
+
+    it('requires the provided parameter "foreignKey" to be a string', async function () {
+      const S = new Schema();
+      const R = S.get(ReferencesManyResolver);
+      const error = v =>
+        format(
+          'The provided parameter "foreignKey" of ReferencesManyResolver.includeTo ' +
+            'should be a String, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo([], 'sourceName', 'targetName', 'relationName', v);
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+      await expect(throwable({})).to.be.rejectedWith(error('Object'));
+    });
+
+    it('requires the provided parameter "scope" to be an object', async function () {
+      const S = new Schema();
+      const R = S.get(ReferencesManyResolver);
+      const error = v =>
+        format(
+          'The provided parameter "scope" of ReferencesManyResolver.includeTo ' +
+            'should be an Object, but %s given.',
+          v,
+        );
+      const throwable = v =>
+        R.includeTo(
+          [],
+          'sourceName',
+          'targetName',
+          'relationName',
+          undefined,
+          v,
+        );
+      await expect(throwable('str')).to.be.rejectedWith(error('"str"'));
+      await expect(throwable(10)).to.be.rejectedWith(error('10'));
+      await expect(throwable(true)).to.be.rejectedWith(error('true'));
+      await expect(throwable([])).to.be.rejectedWith(error('Array'));
+    });
+
+    it('throws an error if the given target model is not found', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'source'});
+      const R = S.get(ReferencesManyResolver);
+      const promise = R.includeTo([], 'source', 'target', 'relation');
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" is not defined',
+      );
+    });
+
+    it('throws an error if the given target model does not have a datasource', async function () {
+      const S = new Schema();
+      S.defineModel({name: 'target'});
+      const R = S.get(ReferencesManyResolver);
+      const promise = R.includeTo([], 'source', 'target', 'relation');
+      await expect(promise).to.be.rejectedWith(
+        'The model "target" does not have a specified datasource.',
+      );
+    });
+
+    it('does not throw an error if a relation target is not found', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRel = S.getRepository('source');
+      const source = await sourceRel.create({parentIds: [10, 20]});
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'parents');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: [10, 20],
+        parents: [],
+      });
+    });
+
+    it('includes if a primary key is not defined in the target model', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target1 = await targetRep.create({});
+      const target2 = await targetRep.create({});
+      const target3 = await targetRep.create({});
+      expect(target1).to.be.eql({[DEF_PK]: target1[DEF_PK]});
+      expect(target2).to.be.eql({[DEF_PK]: target2[DEF_PK]});
+      expect(target3).to.be.eql({[DEF_PK]: target3[DEF_PK]});
+      const source = await sourceRep.create({
+        parentIds: [target1[DEF_PK], target2[DEF_PK]],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: [target1[DEF_PK], target2[DEF_PK]],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'parents');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        parents: [target1, target2],
+      });
+    });
+
+    it('includes if the target model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({
+        name: 'target',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target1 = await targetRep.create({});
+      const target2 = await targetRep.create({});
+      const target3 = await targetRep.create({});
+      expect(target1).to.be.eql({myId: target1.myId});
+      expect(target2).to.be.eql({myId: target2.myId});
+      expect(target3).to.be.eql({myId: target3.myId});
+      const source = await sourceRep.create({
+        parentIds: [target1.myId, target2.myId],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: [target1.myId, target2.myId],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'parents');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        parents: [target1, target2],
+      });
+    });
+
+    it('includes if the source model has a custom primary key', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({
+        name: 'source',
+        datasource: 'datasource',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target1 = await targetRep.create({});
+      const target2 = await targetRep.create({});
+      const target3 = await targetRep.create({});
+      expect(target1).to.be.eql({[DEF_PK]: target1[DEF_PK]});
+      expect(target2).to.be.eql({[DEF_PK]: target2[DEF_PK]});
+      expect(target3).to.be.eql({[DEF_PK]: target3[DEF_PK]});
+      const source = await sourceRep.create({
+        parentIds: [target1[DEF_PK], target2[DEF_PK]],
+      });
+      expect(source).to.be.eql({
+        myId: source.myId,
+        parentIds: [target1[DEF_PK], target2[DEF_PK]],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'parents');
+      expect(source).to.be.eql({
+        myId: source.myId,
+        parentIds: source.parentIds,
+        parents: [target1, target2],
+      });
+    });
+
+    it('includes if the property "foreignKey" is specified', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target1 = await targetRep.create({});
+      const target2 = await targetRep.create({});
+      const target3 = await targetRep.create({});
+      expect(target1).to.be.eql({[DEF_PK]: target1[DEF_PK]});
+      expect(target2).to.be.eql({[DEF_PK]: target2[DEF_PK]});
+      expect(target3).to.be.eql({[DEF_PK]: target3[DEF_PK]});
+      const source = await sourceRep.create({
+        parentIds: [target1[DEF_PK], target2[DEF_PK]],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: [target1[DEF_PK], target2[DEF_PK]],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'relations', 'parentIds');
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        relations: [target1, target2],
+      });
+    });
+
+    it('uses a where clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target1 = await targetRep.create({featured: false});
+      const target2 = await targetRep.create({featured: true});
+      const target3 = await targetRep.create({featured: true});
+      expect(target1).to.be.eql({[DEF_PK]: target1[DEF_PK], featured: false});
+      expect(target2).to.be.eql({[DEF_PK]: target2[DEF_PK], featured: true});
+      expect(target3).to.be.eql({[DEF_PK]: target3[DEF_PK], featured: true});
+      const source = await sourceRep.create({
+        parentIds: [target1[DEF_PK], target2[DEF_PK], target3[DEF_PK]],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: [target1[DEF_PK], target2[DEF_PK], target3[DEF_PK]],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'parents', undefined, {
+        where: {featured: false},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        parents: [target1],
+      });
+      delete source.parents;
+      await R.includeTo([source], 'source', 'target', 'parents', undefined, {
+        where: {featured: true},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        parents: [target2, target3],
+      });
+    });
+
+    it('uses a slice clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target1 = await targetRep.create({});
+      const target2 = await targetRep.create({});
+      const target3 = await targetRep.create({});
+      const target4 = await targetRep.create({});
+      expect(target1).to.be.eql({[DEF_PK]: target1[DEF_PK]});
+      expect(target2).to.be.eql({[DEF_PK]: target2[DEF_PK]});
+      expect(target3).to.be.eql({[DEF_PK]: target3[DEF_PK]});
+      expect(target4).to.be.eql({[DEF_PK]: target4[DEF_PK]});
+      const source = await sourceRep.create({
+        parentIds: [
+          target1[DEF_PK],
+          target2[DEF_PK],
+          target3[DEF_PK],
+          target4[DEF_PK],
+        ],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: [
+          target1[DEF_PK],
+          target2[DEF_PK],
+          target3[DEF_PK],
+          target4[DEF_PK],
+        ],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'parents', undefined, {
+        skip: 1,
+        limit: 2,
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        parents: [target2, target3],
+      });
+    });
+
+    it('uses a fields clause of the given scope to filter the relation target', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target1 = await targetRep.create({
+        foo: 'fooVal1',
+        bar: 'barVal1',
+      });
+      const target2 = await targetRep.create({
+        foo: 'fooVal2',
+        bar: 'barVal2',
+      });
+      const target3 = await targetRep.create({
+        foo: 'fooVal3',
+        bar: 'barVal3',
+      });
+      expect(target1).to.be.eql({
+        [DEF_PK]: target1[DEF_PK],
+        foo: 'fooVal1',
+        bar: 'barVal1',
+      });
+      expect(target2).to.be.eql({
+        [DEF_PK]: target2[DEF_PK],
+        foo: 'fooVal2',
+        bar: 'barVal2',
+      });
+      expect(target3).to.be.eql({
+        [DEF_PK]: target3[DEF_PK],
+        foo: 'fooVal3',
+        bar: 'barVal3',
+      });
+      const source = await sourceRep.create({
+        parentIds: [target1[DEF_PK], target2[DEF_PK]],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: [target1[DEF_PK], target2[DEF_PK]],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'parents', undefined, {
+        fields: [DEF_PK, 'bar'],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        parents: [
+          {
+            [DEF_PK]: target1[DEF_PK],
+            bar: target1.bar,
+          },
+          {
+            [DEF_PK]: target2[DEF_PK],
+            bar: target2.bar,
+          },
+        ],
+      });
+    });
+
+    it('uses an include clause of the given scope to resolve target relations', async function () {
+      const S = new Schema();
+      S.defineDatasource({
+        name: 'datasource',
+        adapter: 'memory',
+      });
+      S.defineModel({
+        name: 'modelA',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelA',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelB',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelB',
+          },
+        },
+        relations: {
+          parent: {
+            type: RelationType.BELONGS_TO,
+            model: 'modelA',
+          },
+        },
+      });
+      S.defineModel({
+        name: 'modelC',
+        datasource: 'datasource',
+        properties: {
+          id: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+          source: {
+            type: DataType.STRING,
+            default: 'modelC',
+          },
+        },
+        relations: {
+          parents: {
+            type: RelationType.REFERENCES_MANY,
+            model: 'modelB',
+          },
+        },
+      });
+      const aRep = S.getRepository('modelA');
+      const bRep = S.getRepository('modelB');
+      const cRep = S.getRepository('modelC');
+      const a1 = await aRep.create({});
+      const a2 = await aRep.create({});
+      const b1 = await bRep.create({parentId: a1.id});
+      const b2 = await bRep.create({parentId: a2.id});
+      const c = await cRep.create({parentIds: [b1.id, b2.id]});
+      expect(a1).to.be.eql({
+        id: a1.id,
+        source: 'modelA',
+      });
+      expect(a2).to.be.eql({
+        id: a2.id,
+        source: 'modelA',
+      });
+      expect(b1).to.be.eql({
+        id: b1.id,
+        source: 'modelB',
+        parentId: a1.id,
+      });
+      expect(b2).to.be.eql({
+        id: b2.id,
+        source: 'modelB',
+        parentId: a2.id,
+      });
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentIds: [b1.id, b2.id],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([c], 'modelC', 'modelB', 'parents', undefined, {
+        include: 'parent',
+      });
+      expect(c).to.be.eql({
+        id: c.id,
+        source: 'modelC',
+        parentIds: [b1.id, b2.id],
+        parents: [
+          {
+            id: b1.id,
+            source: 'modelB',
+            parentId: a1.id,
+            parent: {
+              id: a1.id,
+              source: 'modelA',
+            },
+          },
+          {
+            id: b2.id,
+            source: 'modelB',
+            parentId: a2.id,
+            parent: {
+              id: a2.id,
+              source: 'modelA',
+            },
+          },
+        ],
+      });
+    });
+
+    it('does not break the "and" operator of the given "where" clause', async function () {
+      const S = new Schema();
+      S.defineDatasource({name: 'datasource', adapter: 'memory'});
+      S.defineModel({name: 'source', datasource: 'datasource'});
+      S.defineModel({name: 'target', datasource: 'datasource'});
+      const sourceRep = S.getRepository('source');
+      const targetRep = S.getRepository('target');
+      const target1 = await targetRep.create({featured: false});
+      const target2 = await targetRep.create({featured: true});
+      const target3 = await targetRep.create({featured: true});
+      expect(target1).to.be.eql({[DEF_PK]: target1[DEF_PK], featured: false});
+      expect(target2).to.be.eql({[DEF_PK]: target2[DEF_PK], featured: true});
+      expect(target3).to.be.eql({[DEF_PK]: target3[DEF_PK], featured: true});
+      const source = await sourceRep.create({
+        parentIds: [target1[DEF_PK], target2[DEF_PK], target3[DEF_PK]],
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: [target1[DEF_PK], target2[DEF_PK], target3[DEF_PK]],
+      });
+      const R = S.get(ReferencesManyResolver);
+      await R.includeTo([source], 'source', 'target', 'parents', undefined, {
+        where: {and: [{featured: false}]},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        parents: [target1],
+      });
+      delete source.parents;
+      await R.includeTo([source], 'source', 'target', 'parents', undefined, {
+        where: {and: [{featured: true}]},
+      });
+      expect(source).to.be.eql({
+        [DEF_PK]: source[DEF_PK],
+        parentIds: source.parentIds,
+        parents: [target2, target3],
+      });
+    });
+  });
+});

+ 3 - 0
src/repository/index.js

@@ -0,0 +1,3 @@
+export * from './repository.js';
+export * from './repository-observer.js';
+export * from './repository-registry.js';

+ 165 - 0
src/repository/repository-observer.js

@@ -0,0 +1,165 @@
+import {Service} from '../service/index.js';
+import {isPromise} from 'mocha/lib/utils.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {DefinitionRegistry} from '../definition/index.js';
+
+/**
+ * Repository event.
+ *
+ * @type {{
+ *   BEFORE_UPDATE: string,
+ *   AFTER_READ: string,
+ *   BEFORE_READ: string,
+ *   AFTER_DELETE: string,
+ *   AFTER_CREATE: string,
+ *   BEFORE_DELETE: string,
+ *   BEFORE_CREATE: string,
+ *   AFTER_UPDATE: string,
+ * }}
+ */
+export const RepositoryEvent = {
+  BEFORE_CREATE: 'beforeCreate',
+  BEFORE_READ: 'beforeRead',
+  BEFORE_UPDATE: 'beforeUpdate',
+  BEFORE_DELETE: 'beforeDelete',
+  AFTER_CREATE: 'afterCreate',
+  AFTER_READ: 'afterRead',
+  AFTER_UPDATE: 'afterUpdate',
+  AFTER_DELETE: 'afterDelete',
+};
+
+/**
+ * Repository observer.
+ */
+export class RepositoryObserver extends Service {
+  /**
+   * Handlers map.
+   *
+   * @example
+   * ```
+   * const eventsMap = this._handlersMap.get(modelName) || new Map();
+   * const handlers = eventsMap.get(eventName) || [];
+   * ```
+   *
+   * @type {Map<any, any>}
+   * @private
+   */
+  _handlersMap = new Map();
+
+  /**
+   * Observe.
+   *
+   * @param modelName
+   * @param eventName
+   * @param handler
+   */
+  observe(modelName, eventName, handler) {
+    if (arguments.length === 2) {
+      handler = eventName;
+      eventName = modelName;
+      modelName = null;
+    }
+    if (modelName || arguments.length === 3) {
+      if (!modelName || typeof modelName !== 'string')
+        throw new InvalidArgumentError(
+          'The parameter "modelName" of RepositoryObserver.observe ' +
+            'must be a non-empty String, but %s given.',
+          modelName,
+        );
+      if (!this.get(DefinitionRegistry).hasModel(modelName))
+        throw new InvalidArgumentError(
+          'Cannot observe repository of a not defined model %s.',
+          modelName,
+        );
+    }
+    if (!eventName || typeof eventName !== 'string') {
+      throw new InvalidArgumentError(
+        'The parameter "eventName" of RepositoryObserver.observe ' +
+          'must be a non-empty String, but %s given.',
+        eventName,
+      );
+    }
+    if (!handler || typeof handler !== 'function')
+      throw new InvalidArgumentError(
+        'The parameter "handler" of RepositoryObserver.observe ' +
+          'must be a Function, but %s given.',
+        handler,
+      );
+    const eventsMap = this._handlersMap.get(modelName) ?? new Map();
+    const handlers = eventsMap.get(eventName) ?? [];
+    handlers.push(handler);
+    eventsMap.set(eventName, handlers);
+    this._handlersMap.set(modelName, eventsMap);
+  }
+
+  /**
+   * Get handlers.
+   *
+   * @param {string} modelName
+   * @param {string} eventName
+   * @return {function[]}
+   * @private
+   */
+  _getHandlers(modelName, eventName) {
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "modelName" of RepositoryObserver._getHandlers ' +
+          'must be a non-empty String, but %s given.',
+        modelName,
+      );
+    if (!eventName || typeof eventName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "eventName" of RepositoryObserver._getHandlers ' +
+          'must be a non-empty String, but %s given.',
+        eventName,
+      );
+    const rootEventsMap = this._handlersMap.get(null) ?? new Map();
+    const rootHandlers = rootEventsMap.get(eventName) ?? [];
+    const modelEventsMap = this._handlersMap.get(modelName) ?? new Map();
+    const modelHandlers = modelEventsMap.get(eventName) ?? [];
+    return [...rootHandlers, ...modelHandlers];
+  }
+
+  /**
+   * Emit.
+   *
+   * @param {string} modelName
+   * @param {string} eventName
+   * @param {string} methodName
+   * @param {object} context
+   * @return {Promise<unknown[]>}
+   */
+  emit(modelName, eventName, methodName, context) {
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "modelName" of RepositoryObserver.emit ' +
+          'must be a non-empty String, but %s given.',
+        modelName,
+      );
+    if (!eventName || typeof eventName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "eventName" of RepositoryObserver.emit ' +
+          'must be a non-empty String, but %s given.',
+        eventName,
+      );
+    if (!methodName || typeof methodName !== 'string')
+      throw new InvalidArgumentError(
+        'The parameter "methodName" of RepositoryObserver.emit ' +
+          'must be a non-empty String, but %s given.',
+        methodName,
+      );
+    if (!context || typeof context !== 'object' || Array.isArray(context))
+      throw new InvalidArgumentError(
+        'The parameter "context" of RepositoryObserver.emit ' +
+          'must be an Object, but %s given.',
+        context,
+      );
+    const promises = [];
+    const handlers = this._getHandlers(modelName, eventName);
+    handlers.forEach(handler => {
+      const result = handler({modelName, eventName, methodName, ...context});
+      if (isPromise(result)) promises.push(result);
+    });
+    return Promise.all(promises);
+  }
+}

+ 455 - 0
src/repository/repository-observer.spec.js

@@ -0,0 +1,455 @@
+import {expect} from 'chai';
+import {format} from 'util';
+import {Schema} from '../schema.js';
+import {RepositoryEvent} from './repository-observer.js';
+import {RepositoryObserver} from './repository-observer.js';
+
+const MODEL_NAME = 'model';
+const EVENT_NAME = RepositoryEvent.AFTER_CREATE;
+const METHOD_NAME = 'methodName';
+const HANDLER_FN = () => undefined;
+
+describe('RepositoryObserver', function () {
+  describe('observe', function () {
+    describe('2 arguments', function () {
+      it('adds an event handler of the root scope', function () {
+        const schema = new Schema();
+        schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+        schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+        const observer = schema.get(RepositoryObserver);
+        observer.observe(EVENT_NAME, HANDLER_FN);
+        const eventsMap = observer._handlersMap.get(null);
+        const listeners = eventsMap.get(EVENT_NAME);
+        expect(listeners).to.have.lengthOf(1);
+        expect(listeners).to.include(HANDLER_FN);
+      });
+
+      it('requires the "eventName" argument to be RepositoryEvent', function () {
+        const schema = new Schema();
+        const observer = schema.get(RepositoryObserver);
+        const throwable = v => () => observer.observe(v, HANDLER_FN);
+        const error = v =>
+          format(
+            'The parameter "eventName" of RepositoryObserver.observe ' +
+              'must be a non-empty String, but %s given.',
+            v,
+          );
+        expect(throwable()).to.throw(error('undefined'));
+        expect(throwable('')).to.throw(error('""'));
+        expect(throwable(10)).to.throw(error('10'));
+        expect(throwable(true)).to.throw(error('true'));
+        expect(throwable(false)).to.throw(error('false'));
+        expect(throwable([])).to.throw(error('Array'));
+        expect(throwable({})).to.throw(error('Object'));
+        expect(throwable(null)).to.throw(error('null'));
+        Object.values(RepositoryEvent).forEach(e => throwable(e)());
+      });
+
+      it('requires the "handler" argument to be a function', function () {
+        const schema = new Schema();
+        const observer = schema.get(RepositoryObserver);
+        const throwable = v => () => observer.observe(EVENT_NAME, v);
+        const error = v =>
+          format(
+            'The parameter "handler" of RepositoryObserver.observe ' +
+              'must be a Function, but %s given.',
+            v,
+          );
+        expect(throwable()).to.throw(error('undefined'));
+        expect(throwable('')).to.throw(error('""'));
+        expect(throwable('str')).to.throw(error('"str"'));
+        expect(throwable(10)).to.throw(error('10'));
+        expect(throwable(true)).to.throw(error('true'));
+        expect(throwable(false)).to.throw(error('false'));
+        expect(throwable([])).to.throw(error('Array'));
+        expect(throwable({})).to.throw(error('Object'));
+        expect(throwable(null)).to.throw(error('null'));
+        throwable(HANDLER_FN)();
+      });
+    });
+
+    describe('3 arguments', function () {
+      it('adds an event handler of a model scope', function () {
+        const schema = new Schema();
+        schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+        schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+        const observer = schema.get(RepositoryObserver);
+        observer.observe(MODEL_NAME, EVENT_NAME, HANDLER_FN);
+        const eventsMap = observer._handlersMap.get(MODEL_NAME);
+        const listeners = eventsMap.get(EVENT_NAME);
+        expect(listeners).to.have.lengthOf(1);
+        expect(listeners).to.include(HANDLER_FN);
+      });
+
+      it('requires the "modelName" argument to be a non-empty string', function () {
+        const schema = new Schema();
+        schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+        schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+        const observer = schema.get(RepositoryObserver);
+        const throwable = v => () =>
+          observer.observe(v, EVENT_NAME, HANDLER_FN);
+        const error = v =>
+          format(
+            'The parameter "modelName" of RepositoryObserver.observe ' +
+              'must be a non-empty String, but %s given.',
+            v,
+          );
+        expect(throwable()).to.throw(error('undefined'));
+        expect(throwable('')).to.throw(error('""'));
+        expect(throwable(10)).to.throw(error('10'));
+        expect(throwable(true)).to.throw(error('true'));
+        expect(throwable(false)).to.throw(error('false'));
+        expect(throwable([])).to.throw(error('Array'));
+        expect(throwable({})).to.throw(error('Object'));
+        expect(throwable(null)).to.throw(error('null'));
+        throwable(MODEL_NAME)();
+      });
+
+      it('requires the "modelName" argument to be a defined model', function () {
+        const schema = new Schema();
+        const observer = schema.get(RepositoryObserver);
+        const throwable = () =>
+          observer.observe('unknown', EVENT_NAME, HANDLER_FN);
+        expect(throwable).to.throw(
+          'Cannot observe repository of a not defined model "unknown".',
+        );
+        schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+        schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+        observer.observe(MODEL_NAME, EVENT_NAME, HANDLER_FN);
+      });
+
+      it('requires the "eventName" argument to be RepositoryEvent', function () {
+        const schema = new Schema();
+        schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+        schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+        const observer = schema.get(RepositoryObserver);
+        const throwable = v => () =>
+          observer.observe(MODEL_NAME, v, HANDLER_FN);
+        const error = v =>
+          format(
+            'The parameter "eventName" of RepositoryObserver.observe ' +
+              'must be a non-empty String, but %s given.',
+            v,
+          );
+        expect(throwable()).to.throw(error('undefined'));
+        expect(throwable('')).to.throw(error('""'));
+        expect(throwable(10)).to.throw(error('10'));
+        expect(throwable(true)).to.throw(error('true'));
+        expect(throwable(false)).to.throw(error('false'));
+        expect(throwable([])).to.throw(error('Array'));
+        expect(throwable({})).to.throw(error('Object'));
+        expect(throwable(null)).to.throw(error('null'));
+        Object.values(RepositoryEvent).forEach(e => throwable(e)());
+      });
+
+      it('requires the "handler" argument to be a function', function () {
+        const schema = new Schema();
+        schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+        schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+        const observer = schema.get(RepositoryObserver);
+        const throwable = v => () =>
+          observer.observe(MODEL_NAME, EVENT_NAME, v);
+        const error = v =>
+          format(
+            'The parameter "handler" of RepositoryObserver.observe ' +
+              'must be a Function, but %s given.',
+            v,
+          );
+        expect(throwable()).to.throw(error('undefined'));
+        expect(throwable('')).to.throw(error('""'));
+        expect(throwable('str')).to.throw(error('"str"'));
+        expect(throwable(10)).to.throw(error('10'));
+        expect(throwable(true)).to.throw(error('true'));
+        expect(throwable(false)).to.throw(error('false'));
+        expect(throwable([])).to.throw(error('Array'));
+        expect(throwable({})).to.throw(error('Object'));
+        expect(throwable(null)).to.throw(error('null'));
+        throwable(HANDLER_FN)();
+      });
+    });
+  });
+
+  describe('_getHandlers', function () {
+    it('returns an empty array if no handlers', function () {
+      const schema = new Schema();
+      const observer = schema.get(RepositoryObserver);
+      const result = observer._getHandlers(MODEL_NAME, EVENT_NAME);
+      expect(result).to.have.lengthOf(0);
+    });
+
+    it('returns model handlers that includes root handlers', function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: 'modelA', datasource: 'datasource'});
+      schema.defineModel({name: 'modelB', datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const handler1 = () => undefined;
+      const handler2 = () => undefined;
+      const handler3 = () => undefined;
+      const handler4 = () => undefined;
+      const handler5 = () => undefined;
+      const handler6 = () => undefined;
+      const handler7 = () => undefined;
+      const event1 = 'event1';
+      const event2 = 'event2';
+      observer.observe(event1, handler1);
+      observer.observe(event1, handler2);
+      observer.observe(event2, handler3);
+      observer.observe('modelA', event1, handler4);
+      observer.observe('modelA', event1, handler5);
+      observer.observe('modelA', event2, handler6);
+      observer.observe('modelB', event1, handler7);
+      const result = observer._getHandlers('modelA', event1);
+      expect(result).to.be.eql([handler1, handler2, handler4, handler5]);
+    });
+
+    it('requires the "modelName" argument to be a non-empty string', function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const throwable = v => () => observer._getHandlers(v, EVENT_NAME);
+      const error = v =>
+        format(
+          'The parameter "modelName" of RepositoryObserver._getHandlers ' +
+            'must be a non-empty String, but %s given.',
+          v,
+        );
+      expect(throwable()).to.throw(error('undefined'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      throwable(MODEL_NAME)();
+    });
+
+    it('requires the "eventName" argument to be a non-empty string', function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const throwable = v => () => observer._getHandlers(MODEL_NAME, v);
+      const error = v =>
+        format(
+          'The parameter "eventName" of RepositoryObserver._getHandlers ' +
+            'must be a non-empty String, but %s given.',
+          v,
+        );
+      expect(throwable()).to.throw(error('undefined'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      throwable('eventName')();
+    });
+  });
+
+  describe('emit', function () {
+    it('requires the "modelName" argument to be a non-empty string', function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const throwable = v => () =>
+        observer.emit(v, EVENT_NAME, METHOD_NAME, {});
+      const error = v =>
+        format(
+          'The parameter "modelName" of RepositoryObserver.emit ' +
+            'must be a non-empty String, but %s given.',
+          v,
+        );
+      expect(throwable()).to.throw(error('undefined'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      throwable(MODEL_NAME)();
+    });
+
+    it('requires the "eventName" argument to be a non-empty string', function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const throwable = v => () =>
+        observer.emit(MODEL_NAME, v, METHOD_NAME, {});
+      const error = v =>
+        format(
+          'The parameter "eventName" of RepositoryObserver.emit ' +
+            'must be a non-empty String, but %s given.',
+          v,
+        );
+      expect(throwable()).to.throw(error('undefined'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      throwable('eventName')();
+    });
+
+    it('requires the "methodName" argument to be a non-empty string', function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const throwable = v => () => observer.emit(MODEL_NAME, EVENT_NAME, v, {});
+      const error = v =>
+        format(
+          'The parameter "methodName" of RepositoryObserver.emit ' +
+            'must be a non-empty String, but %s given.',
+          v,
+        );
+      expect(throwable()).to.throw(error('undefined'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable(null)).to.throw(error('null'));
+      throwable(METHOD_NAME)();
+    });
+
+    it('requires the "context" argument to be a non-empty string', function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const throwable = v => () =>
+        observer.emit(MODEL_NAME, EVENT_NAME, METHOD_NAME, v);
+      const error = v =>
+        format(
+          'The parameter "context" of RepositoryObserver.emit ' +
+            'must be an Object, but %s given.',
+          v,
+        );
+      expect(throwable()).to.throw(error('undefined'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(null)).to.throw(error('null'));
+      throwable({})();
+    });
+
+    it('returns a promise for synchronous handlers', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      schema.defineModel({name: 'modelB', datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const order = [];
+      const handler1 = () => order.push(handler1);
+      const handler2 = () => order.push(handler2);
+      const handler3 = () => order.push(handler3);
+      const handler4 = () => order.push(handler4);
+      const throwable = () => {
+        throw new Error();
+      };
+      observer.observe(EVENT_NAME, handler1);
+      observer.observe(EVENT_NAME, handler2);
+      observer.observe('event2', throwable);
+      observer.observe(MODEL_NAME, EVENT_NAME, handler3);
+      observer.observe(MODEL_NAME, EVENT_NAME, handler4);
+      observer.observe(MODEL_NAME, 'event2', throwable);
+      observer.observe('modelB', EVENT_NAME, throwable);
+      const promise = observer.emit(MODEL_NAME, EVENT_NAME, METHOD_NAME, {});
+      expect(promise).to.be.instanceof(Promise);
+      await promise;
+      expect(order).to.be.eql([handler1, handler2, handler3, handler4]);
+    });
+
+    it('returns a promise of asynchronous handlers', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      schema.defineModel({name: 'modelB', datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const order = [];
+      const handler0ms = () => {
+        order.push(handler0ms);
+        return Promise.resolve();
+      };
+      const handler5ms = () => {
+        return new Promise(res => {
+          setTimeout(() => {
+            order.push(handler5ms);
+            res();
+          }, 5);
+        });
+      };
+      const syncHandler = () => order.push(syncHandler);
+      const handler3ms = () => {
+        return new Promise(res => {
+          setTimeout(() => {
+            order.push(handler3ms);
+            res();
+          }, 3);
+        });
+      };
+      const throwable = () => {
+        throw new Error();
+      };
+      observer.observe(EVENT_NAME, handler0ms);
+      observer.observe(EVENT_NAME, handler5ms);
+      observer.observe('event2', throwable);
+      observer.observe(MODEL_NAME, EVENT_NAME, syncHandler);
+      observer.observe(MODEL_NAME, EVENT_NAME, handler3ms);
+      observer.observe(MODEL_NAME, 'event2', throwable);
+      observer.observe('modelB', EVENT_NAME, throwable);
+      const promise = observer.emit(MODEL_NAME, EVENT_NAME, METHOD_NAME, {});
+      expect(promise).to.be.instanceof(Promise);
+      await promise;
+      expect(order).to.have.lengthOf(4);
+      expect(order).to.include(handler0ms);
+      expect(order).to.include(handler5ms);
+      expect(order).to.include(syncHandler);
+      expect(order).to.include(handler3ms);
+    });
+
+    it('executes handlers with the context object', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: MODEL_NAME, datasource: 'datasource'});
+      const observer = schema.get(RepositoryObserver);
+      const inputContext = {customProp: 'customProp'};
+      const context = {
+        modelName: MODEL_NAME,
+        eventName: EVENT_NAME,
+        methodName: METHOD_NAME,
+        ...inputContext,
+      };
+      let counter = 0;
+      const handler1 = ctx => {
+        expect(ctx).to.be.eql(context);
+        counter++;
+      };
+      const handler2 = ctx => {
+        return new Promise(res => {
+          setTimeout(() => {
+            expect(ctx).to.be.eql(context);
+            counter++;
+            res();
+          }, 3);
+        });
+      };
+      observer.observe(EVENT_NAME, handler1);
+      observer.observe(MODEL_NAME, EVENT_NAME, handler2);
+      await observer.emit(MODEL_NAME, EVENT_NAME, METHOD_NAME, inputContext);
+      expect(counter).to.be.eq(2);
+    });
+  });
+});

+ 54 - 0
src/repository/repository-registry.js

@@ -0,0 +1,54 @@
+import {Repository} from './repository.js';
+import {Service} from '../service/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+
+/**
+ * Repository registry.
+ */
+export class RepositoryRegistry extends Service {
+  /**
+   * Repositories.
+   */
+  _repositories = {};
+
+  /**
+   * Repository ctor.
+   *
+   * @type {typeof Repository}
+   * @private
+   */
+  _repositoryCtor = Repository;
+
+  /**
+   * Set repository ctor.
+   *
+   * @param ctor
+   */
+  setRepositoryCtor(ctor) {
+    if (
+      !ctor ||
+      typeof ctor !== 'function' ||
+      !(ctor.prototype instanceof Repository)
+    ) {
+      throw new InvalidArgumentError(
+        'The first argument of RepositoryRegistry.setRepositoryCtor ' +
+          'must inherit from Repository class, but %s given.',
+        ctor,
+      );
+    }
+    this._repositoryCtor = ctor;
+  }
+
+  /**
+   * Get repository.
+   *
+   * @param modelName
+   */
+  getRepository(modelName) {
+    let repository = this._repositories[modelName];
+    if (repository) return repository;
+    repository = new this._repositoryCtor(this._services, modelName);
+    this._repositories[modelName] = repository;
+    return repository;
+  }
+}

+ 38 - 0
src/repository/repository-registry.spec.js

@@ -0,0 +1,38 @@
+import {expect} from 'chai';
+import {Schema} from '../schema.js';
+import {Repository} from './repository.js';
+import {RepositoryRegistry} from './repository-registry.js';
+
+describe('RepositoryRegistry', function () {
+  describe('setRepositoryCtor', function () {
+    it('sets a given class as the repository constructor', function () {
+      class MyRepository extends Repository {}
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: 'model', datasource: 'datasource'});
+      const registry = schema.get(RepositoryRegistry);
+      registry.setRepositoryCtor(MyRepository);
+      const rep = registry.getRepository('model');
+      expect(rep).to.be.instanceof(Repository);
+      expect(rep).to.be.instanceof(MyRepository);
+    });
+  });
+
+  describe('getRepository', function () {
+    it('uses a given model name to return an existing repository or create the new', function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: 'modelA', datasource: 'datasource'});
+      schema.defineModel({name: 'modelB', datasource: 'datasource'});
+      const registry = schema.get(RepositoryRegistry);
+      const repA1 = registry.getRepository('modelA');
+      const repA2 = registry.getRepository('modelA');
+      const repB1 = registry.getRepository('modelB');
+      const repB2 = registry.getRepository('modelB');
+      expect(repA1).to.be.eq(repA2);
+      expect(repB1).to.be.eq(repB2);
+      expect(repA1).to.be.not.eq(repB1);
+      expect(repA2).to.be.not.eq(repB2);
+    });
+  });
+});

+ 373 - 0
src/repository/repository.js

@@ -0,0 +1,373 @@
+import {Service} from '../service/index.js';
+import {AdapterRegistry} from '../adapter/index.js';
+import {InvalidArgumentError} from '../errors/index.js';
+import {RepositoryEvent} from './repository-observer.js';
+import {DefinitionRegistry} from '../definition/index.js';
+import {ModelDefinitionUtils} from '../definition/index.js';
+import {RepositoryObserver} from './repository-observer.js';
+
+/**
+ * Repository method.
+ *
+ * @type {{
+ *   DELETE: string,
+ *   DELETE_BY_ID: string,
+ *   CREATE: string,
+ *   EXISTS: string,
+ *   PATCH_BY_ID: string,
+ *   FIND: string,
+ *   FIND_BY_ID: string,
+ *   COUNT: string,
+ *   REPLACE_BY_ID: string,
+ *   REPLACE_OR_CREATE: string,
+ * }}
+ */
+export const RepositoryMethod = {
+  CREATE: 'create',
+  REPLACE_BY_ID: 'replaceById',
+  REPLACE_OR_CREATE: 'replaceOrCreate',
+  PATCH_BY_ID: 'patchById',
+  FIND: 'find',
+  FIND_ONE: 'findOne',
+  FIND_BY_ID: 'findById',
+  DELETE: 'delete',
+  DELETE_BY_ID: 'deleteById',
+  EXISTS: 'exists',
+  COUNT: 'count',
+};
+
+/**
+ * Repository.
+ */
+export class Repository extends Service {
+  /**
+   * Model name.
+   */
+  _modelName;
+
+  /**
+   * Model name.
+   *
+   * @return {string}
+   */
+  get modelName() {
+    return this._modelName;
+  }
+
+  /**
+   * Datasource name.
+   */
+  _datasourceName;
+
+  /**
+   * Datasource name.
+   *
+   * @return {string}
+   */
+  get datasourceName() {
+    return this._datasourceName;
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param services
+   * @param modelName
+   */
+  constructor(services, modelName) {
+    super(services);
+    this._modelName = modelName;
+    const modelDef = this.get(DefinitionRegistry).getModel(modelName);
+    const datasourceName = modelDef.datasource;
+    if (!datasourceName)
+      throw new InvalidArgumentError(
+        'The model %s does not have a specified datasource.',
+        modelName,
+      );
+    this._datasourceName = datasourceName;
+  }
+
+  /**
+   * Get adapter.
+   *
+   * @return {Promise<Object>}
+   */
+  async getAdapter() {
+    return this.get(AdapterRegistry).getAdapter(this.datasourceName);
+  }
+
+  /**
+   * Create.
+   *
+   * @param {object} data
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async create(data, filter = undefined) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_CREATE,
+      RepositoryMethod.CREATE,
+      {data, filter},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.create(this.modelName, data, filter);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_CREATE,
+      RepositoryMethod.CREATE,
+      {data, filter, result},
+    );
+    return result;
+  }
+
+  /**
+   * Replace by id.
+   *
+   * @param {number|string} id
+   * @param {object} data
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async replaceById(id, data, filter = undefined) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_UPDATE,
+      RepositoryMethod.REPLACE_BY_ID,
+      {id, data, filter},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.replaceById(this.modelName, id, data, filter);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_UPDATE,
+      RepositoryMethod.REPLACE_BY_ID,
+      {id, data, filter, result},
+    );
+    return result;
+  }
+
+  /**
+   * Replace or create.
+   *
+   * @param {object} data
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async replaceOrCreate(data, filter = undefined) {
+    const pkPropName = this.get(
+      ModelDefinitionUtils,
+    ).getPrimaryKeyAsPropertyName(this.modelName);
+    const pkValue = data[pkPropName];
+    if (pkPropName == null) return this.create(data, filter);
+    return this.replaceById(pkValue, data, filter);
+  }
+
+  /**
+   * Patch by id.
+   *
+   * @param {number|string} id
+   * @param {object} data
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async patchById(id, data, filter = undefined) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_UPDATE,
+      RepositoryMethod.PATCH_BY_ID,
+      {id, data, filter},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.patchById(this.modelName, id, data, filter);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_UPDATE,
+      RepositoryMethod.PATCH_BY_ID,
+      {id, data, filter, result},
+    );
+    return result;
+  }
+
+  /**
+   * Find.
+   *
+   * @param {object|undefined} filter
+   * @return {Promise<object[]>}
+   */
+  async find(filter = undefined) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_READ,
+      RepositoryMethod.FIND,
+      {filter},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.find(this.modelName, filter);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_READ,
+      RepositoryMethod.FIND,
+      {filter, result},
+    );
+    return result;
+  }
+
+  /**
+   * Find one.
+   *
+   * @param {object|undefined} filter
+   * @return {Promise<object|undefined>}
+   */
+  async findOne(filter = undefined) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_READ,
+      RepositoryMethod.FIND_ONE,
+      {filter},
+    );
+    const adapter = await this.getAdapter();
+    filter = filter ?? {};
+    filter.limit = 1;
+    const resultArray = adapter.find(this.modelName, filter);
+    const result = resultArray.length ? resultArray[0] : undefined;
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_READ,
+      RepositoryMethod.FIND_ONE,
+      {filter, result},
+    );
+    return result;
+  }
+
+  /**
+   * Find by id.
+   *
+   * @param {number|string} id
+   * @param {object|undefined} filter
+   * @return {Promise<object>}
+   */
+  async findById(id, filter = undefined) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_READ,
+      RepositoryMethod.FIND_BY_ID,
+      {id, filter},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.findById(this.modelName, id, filter);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_READ,
+      RepositoryMethod.FIND_BY_ID,
+      {id, filter, result},
+    );
+    return result;
+  }
+
+  /**
+   * Delete.
+   *
+   * @param {object|undefined} where
+   * @return {Promise<number>}
+   */
+  async delete(where = undefined) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_DELETE,
+      RepositoryMethod.DELETE,
+      {where},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.delete(this.modelName, where);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_DELETE,
+      RepositoryMethod.DELETE,
+      {where, result},
+    );
+    return result;
+  }
+
+  /**
+   * Delete by id.
+   *
+   * @param {number|string} id
+   * @return {Promise<boolean>}
+   */
+  async deleteById(id) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_DELETE,
+      RepositoryMethod.DELETE_BY_ID,
+      {id},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.deleteById(this.modelName, id);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_DELETE,
+      RepositoryMethod.DELETE_BY_ID,
+      {id, result},
+    );
+    return result;
+  }
+
+  /**
+   * Exists.
+   *
+   * @param {number|string} id
+   * @return {Promise<boolean>}
+   */
+  async exists(id) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_READ,
+      RepositoryMethod.EXISTS,
+      {id},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.exists(this.modelName, id);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_READ,
+      RepositoryMethod.EXISTS,
+      {id, result},
+    );
+    return result;
+  }
+
+  /**
+   * Count.
+   *
+   * @param {object|undefined} where
+   * @return {Promise<number>}
+   */
+  async count(where = undefined) {
+    const observer = this.get(RepositoryObserver);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.BEFORE_READ,
+      RepositoryMethod.COUNT,
+      {where},
+    );
+    const adapter = await this.getAdapter();
+    const result = await adapter.count(this.modelName, where);
+    await observer.emit(
+      this.modelName,
+      RepositoryEvent.AFTER_READ,
+      RepositoryMethod.COUNT,
+      {where, result},
+    );
+    return result;
+  }
+}

+ 125 - 0
src/repository/repository.spec.js

@@ -0,0 +1,125 @@
+import {expect} from 'chai';
+import {Schema} from '../schema.js';
+import {RepositoryMethod} from './repository.js';
+import {RepositoryEvent} from './repository-observer.js';
+import {RepositoryObserver} from './repository-observer.js';
+import {DEFAULT_PRIMARY_KEY_PROPERTY_NAME as DEF_PK} from '../definition/index.js';
+
+describe('Repository', function () {
+  describe('create', function () {
+    it('emits the "beforeCreate" event with specific context', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: 'model', datasource: 'datasource'});
+      let rootHandlerExecuted = false;
+      let modelHandlerExecuted = false;
+      const data = {foo: 'bar'};
+      const filter = {};
+      const context = {
+        modelName: 'model',
+        methodName: RepositoryMethod.CREATE,
+        eventName: RepositoryEvent.BEFORE_CREATE,
+        data,
+        filter,
+      };
+      const rootHandler = ctx => {
+        expect(ctx).to.be.eql(context);
+        rootHandlerExecuted = true;
+      };
+      const modelHandler = ctx => {
+        expect(ctx).to.be.eql(context);
+        modelHandlerExecuted = true;
+      };
+      schema
+        .get(RepositoryObserver)
+        .observe(RepositoryEvent.BEFORE_CREATE, rootHandler);
+      schema
+        .get(RepositoryObserver)
+        .observe('model', RepositoryEvent.BEFORE_CREATE, modelHandler);
+      const rep = schema.getRepository('model');
+      await rep.create(data, filter);
+      expect(rootHandlerExecuted).to.be.true;
+      expect(modelHandlerExecuted).to.be.true;
+    });
+
+    it('emits the "afterCreate" event with specific context', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: 'model', datasource: 'datasource'});
+      let rootHandlerExecuted = false;
+      let modelHandlerExecuted = false;
+      const data = {foo: 'bar'};
+      const filter = {};
+      const context = {
+        modelName: 'model',
+        methodName: RepositoryMethod.CREATE,
+        eventName: RepositoryEvent.AFTER_CREATE,
+        data,
+        filter,
+      };
+      let rootHandlerResult;
+      let modelHandlerResult;
+      const rootHandler = ctx => {
+        expect(ctx).to.containSubset(context);
+        rootHandlerResult = ctx.result;
+        rootHandlerExecuted = true;
+      };
+      const modelHandler = ctx => {
+        expect(ctx).to.containSubset(context);
+        modelHandlerResult = ctx.result;
+        modelHandlerExecuted = true;
+      };
+      schema
+        .get(RepositoryObserver)
+        .observe(RepositoryEvent.AFTER_CREATE, rootHandler);
+      schema
+        .get(RepositoryObserver)
+        .observe('model', RepositoryEvent.AFTER_CREATE, modelHandler);
+      const rep = schema.getRepository('model');
+      const result = await rep.create(data, filter);
+      expect(rootHandlerExecuted).to.be.true;
+      expect(modelHandlerExecuted).to.be.true;
+      expect(result).to.be.eq(rootHandlerResult);
+      expect(result).to.be.eq(modelHandlerResult);
+    });
+  });
+
+  describe('replaceById', function () {
+    it('emits the "beforeUpdate" event with specific context', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: 'model', datasource: 'datasource'});
+      let rootHandlerExecuted = false;
+      let modelHandlerExecuted = false;
+      const data = {foo: 'bar'};
+      const filter = {};
+      const rootHandler = ctx => {
+        expect(ctx).to.be.eql(context);
+        rootHandlerExecuted = true;
+      };
+      const modelHandler = ctx => {
+        expect(ctx).to.be.eql(context);
+        modelHandlerExecuted = true;
+      };
+      schema
+        .get(RepositoryObserver)
+        .observe(RepositoryEvent.BEFORE_UPDATE, rootHandler);
+      schema
+        .get(RepositoryObserver)
+        .observe('model', RepositoryEvent.BEFORE_UPDATE, modelHandler);
+      const rep = schema.getRepository('model');
+      const createdItem = await rep.create(data);
+      const context = {
+        modelName: 'model',
+        methodName: RepositoryMethod.REPLACE_BY_ID,
+        eventName: RepositoryEvent.BEFORE_UPDATE,
+        id: createdItem[DEF_PK],
+        data,
+        filter,
+      };
+      await rep.replaceById(context.id, data, filter);
+      expect(rootHandlerExecuted).to.be.true;
+      expect(modelHandlerExecuted).to.be.true;
+    });
+  });
+});

+ 37 - 0
src/schema.js

@@ -0,0 +1,37 @@
+import {Service} from './service/index.js';
+import {DefinitionRegistry} from './definition/index.js';
+import {RepositoryRegistry} from './repository/index.js';
+
+/**
+ * Schema.
+ */
+export class Schema extends Service {
+  /**
+   * Define datasource.
+   *
+   * @param datasourceDef
+   */
+  defineDatasource(datasourceDef) {
+    this.get(DefinitionRegistry).addDatasource(datasourceDef);
+    return this;
+  }
+
+  /**
+   * Define model.
+   *
+   * @param modelDef
+   */
+  defineModel(modelDef) {
+    this.get(DefinitionRegistry).addModel(modelDef);
+    return this;
+  }
+
+  /**
+   * Get repository.
+   *
+   * @param modelName
+   */
+  getRepository(modelName) {
+    return this.get(RepositoryRegistry).getRepository(modelName);
+  }
+}

+ 1 - 0
src/service/index.js

@@ -0,0 +1 @@
+export * from './service.js';

+ 28 - 0
src/service/service.js

@@ -0,0 +1,28 @@
+/**
+ * Service.
+ */
+export class Service {
+  /**
+   * Constructor.
+   *
+   * @param services
+   */
+  constructor(services = undefined) {
+    this._services = services ?? new Map();
+  }
+
+  /**
+   * Get.
+   *
+   * @template T
+   * @param {typeof T} serviceCtor
+   * @return {T}
+   */
+  get(serviceCtor) {
+    let service = this._services.get(serviceCtor);
+    if (service) return service;
+    service = new serviceCtor(this._services);
+    this._services.set(serviceCtor, service);
+    return service;
+  }
+}

+ 37 - 0
src/service/service.spec.js

@@ -0,0 +1,37 @@
+import {expect} from 'chai';
+import {Service} from './service.js';
+
+describe('Service', function () {
+  describe('constructor', function () {
+    it('sets an empty service map by default', function () {
+      const service = new Service();
+      expect(service._services).to.be.instanceof(Map);
+      expect(service._services).to.be.empty;
+    });
+
+    it('sets a given service map', function () {
+      const map = new Map();
+      const service = new Service(map);
+      expect(service._services).to.be.eq(map);
+    });
+  });
+
+  describe('get', function () {
+    it('returns a new service from a given constructor as a singleton', function () {
+      let executed = 0;
+      class MyService extends Service {
+        constructor() {
+          super();
+          ++executed;
+        }
+      }
+      const service = new Service();
+      const myService1 = service.get(MyService);
+      const myService2 = service.get(MyService);
+      expect(myService1).to.be.instanceof(MyService);
+      expect(myService2).to.be.instanceof(MyService);
+      expect(myService1).to.be.eq(myService2);
+      expect(executed).to.be.eq(1);
+    });
+  });
+});

Some files were not shown because too many files changed in this diff