Просмотр исходного кода

chore: improve "include" option

e22m4u 2 лет назад
Родитель
Сommit
8b671f3e45
2 измененных файлов с 4432 добавлено и 323 удалено
  1. 41 32
      src/filter/include-clause-tool.js
  2. 4391 291
      src/filter/include-clause-tool.spec.js

+ 41 - 32
src/filter/include-clause-tool.js

@@ -163,12 +163,12 @@ export class IncludeClauseTool extends Service {
    * @param {IncludeClause|undefined} clause
    */
   static validateIncludeClause(clause) {
-    if (!clause) {
-      // empty
-    } else if (typeof clause === 'string') {
-      // string
+    if (clause == null) {
+      // allows undefined and null
+    } else if (clause && typeof clause === 'string') {
+      // allows non-empty string
     } else if (Array.isArray(clause)) {
-      // array
+      // validate array
       const relNames = [];
       clause.flat().forEach(el => {
         this.validateIncludeClause(el);
@@ -191,7 +191,7 @@ export class IncludeClauseTool extends Service {
           duplicateNames[0],
         );
     } else if (typeof clause === 'object') {
-      // object
+      // validate object
       if ('relation' in clause) {
         // {relation: 'name', scope: {}}
         if (!clause.relation || typeof clause.relation !== 'string')
@@ -205,14 +205,15 @@ export class IncludeClauseTool extends Service {
         // {foo: 'bar', 'baz': ['qux'], ...}
         Object.keys(clause).forEach(key => {
           if (!Object.prototype.hasOwnProperty.call(clause, key)) return;
+          this.validateIncludeClause(key);
           this.validateIncludeClause(clause[key]);
         });
       }
     } else {
-      // unknown.
+      // unsupported
       throw new InvalidArgumentError(
-        'The provided option "include" should have a value of ' +
-          'following types: String, Object or Array, but %v given.',
+        'The provided option "include" should have a non-empty String, ' +
+          'an Object or an Array, but %v given.',
         clause,
       );
     }
@@ -224,24 +225,30 @@ export class IncludeClauseTool extends Service {
    * @param {object|undefined} clause
    */
   static validateScopeClause(clause) {
-    if (!clause) return;
+    if (clause == null) return;
     if (typeof clause !== 'object' || Array.isArray(clause))
       throw new InvalidArgumentError(
         'The provided option "scope" should be an Object, but %v given.',
         clause,
       );
-    if ('where' in clause && clause.where)
+    if (clause.where != null) {
       WhereClauseTool.validateWhereClause(clause.where);
-    if ('order' in clause && clause.order)
+    }
+    if (clause.order != null) {
       OrderClauseTool.validateOrderClause(clause.order);
-    if ('skip' in clause && clause.skip)
+    }
+    if (clause.skip != null) {
       SliceClauseTool.validateSkipClause(clause.skip);
-    if ('limit' in clause && clause.limit)
+    }
+    if (clause.limit != null) {
       SliceClauseTool.validateLimitClause(clause.limit);
-    if ('fields' in clause && clause.fields)
+    }
+    if (clause.fields != null) {
       FieldsClauseTool.validateFieldsClause(clause.fields);
-    if ('include' in clause && clause.include)
+    }
+    if (clause.include != null) {
       IncludeClauseTool.validateIncludeClause(clause.include);
+    }
   }
 
   /**
@@ -252,14 +259,14 @@ export class IncludeClauseTool extends Service {
    */
   static normalizeIncludeClause(clause) {
     let result = [];
-    if (!clause) {
-      // empty
+    if (clause == null) {
+      // allows undefined and null
       return result;
-    } else if (typeof clause === 'string') {
-      // string
+    } else if (clause && typeof clause === 'string') {
+      // allows non-empty string
       result.push({relation: clause});
     } else if (Array.isArray(clause)) {
-      // array
+      // normalize array
       clause.flat().forEach(el => {
         if (Array.isArray(el)) {
           el = el
@@ -282,7 +289,7 @@ export class IncludeClauseTool extends Service {
           duplicateNames[0],
         );
     } else if (typeof clause === 'object') {
-      // object
+      // normalize object
       if ('relation' in clause) {
         // {relation: 'name', scope: {...}}
         if (!clause.relation || typeof clause.relation !== 'string')
@@ -299,6 +306,7 @@ export class IncludeClauseTool extends Service {
         // {foo: 'bar', baz: ['qux'], ...}
         Object.keys(clause).forEach(key => {
           if (!Object.prototype.hasOwnProperty.call(clause, key)) return;
+          this.validateIncludeClause(key);
           const normalized = {relation: key};
           const include = this.normalizeIncludeClause(clause[key]);
           if (include.length) normalized.scope = {include};
@@ -306,10 +314,10 @@ export class IncludeClauseTool extends Service {
         });
       }
     } else {
-      // unknown
+      // unsupported
       throw new InvalidArgumentError(
-        'The provided option "include" should have a value of ' +
-          'following types: String, Object or Array, but %v given.',
+        'The provided option "include" should have a non-empty String, ' +
+          'an Object or an Array, but %v given.',
         clause,
       );
     }
@@ -323,7 +331,7 @@ export class IncludeClauseTool extends Service {
    * @returns {object|undefined}
    */
   static normalizeScopeClause(clause) {
-    if (!clause) return;
+    if (clause == null) return;
     if (typeof clause !== 'object' || Array.isArray(clause))
       throw new InvalidArgumentError(
         'The provided option "scope" should be an Object, but %v given.',
@@ -331,33 +339,34 @@ export class IncludeClauseTool extends Service {
       );
     const result = {};
     // {where: ...}
-    if ('where' in clause && clause.where) {
+    if (clause.where != null) {
       WhereClauseTool.validateWhereClause(clause.where);
       result.where = clause.where;
     }
     // {order: ...}
-    if ('order' in clause && clause.order) {
+    if (clause.order != null) {
       OrderClauseTool.validateOrderClause(clause.order);
       result.order = clause.order;
     }
     // {skip: ...}
-    if ('skip' in clause && clause.skip) {
+    if (clause.skip != null) {
       SliceClauseTool.validateSkipClause(clause.skip);
       result.skip = clause.skip;
     }
     // {limit: ...}
-    if ('limit' in clause && clause.limit) {
+    if (clause.limit != null) {
       SliceClauseTool.validateLimitClause(clause.limit);
       result.limit = clause.limit;
     }
     // {fields: ...}
-    if ('fields' in clause && clause.fields) {
+    if (clause.fields != null) {
       FieldsClauseTool.validateFieldsClause(clause.fields);
       result.fields = clause.fields;
     }
     // {include: ...}
-    if ('include' in clause && clause.include)
+    if (clause.include != null) {
       result.include = this.normalizeIncludeClause(clause.include);
+    }
     if (Object.keys(result).length) return result;
     return undefined;
   }

+ 4391 - 291
src/filter/include-clause-tool.spec.js

@@ -6,14 +6,12 @@ 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);
+      // undefined and null
       validate(undefined);
-      // strings
+      validate(null);
+      // a non-empty string
       validate('foo');
-      // arrays
+      // an array
       validate(['foo']);
       validate([['foo']]);
       validate([{foo: 'bar'}]);
@@ -25,7 +23,7 @@ describe('IncludeClauseTool', function () {
       validate([{relation: 'foo', scope: {include: ['bar']}}]);
       validate([{relation: 'foo', scope: {include: {bar: 'baz'}}}]);
       validate([{relation: 'foo', scope: {include: [{bar: 'baz'}]}}]);
-      // objects
+      // an object
       validate({foo: 'bar'});
       validate({foo: ['bar']});
       validate({foo: {bar: 'baz'}});
@@ -39,14 +37,14 @@ describe('IncludeClauseTool', function () {
     });
 
     it('throws an error for unsupported values', function () {
-      const validate = v => () => IncludeClauseTool.validateIncludeClause(v);
+      const throwable = v => () => IncludeClauseTool.validateIncludeClause(v);
       const createError = v =>
         format(
-          'The provided option "include" should have a value of ' +
-            'following types: String, Object or Array, but %v given.',
+          'The provided option "include" should have a non-empty String, ' +
+            'an Object or an Array, but %v given.',
           v,
         );
-      const testFor = v => {
+      const testOf = v => {
         const error = createError(v);
         const clauses = [
           v,
@@ -70,11 +68,14 @@ describe('IncludeClauseTool', function () {
           {relation: 'foo', scope: {include: v}},
           {relation: 'foo', scope: {include: {bar: v}}},
         ];
-        clauses.forEach(c => expect(validate(c)).to.throw(error));
+        clauses.forEach(c => expect(throwable(c)).to.throw(error));
       };
-      testFor(10);
-      testFor(true);
-      testFor(() => undefined);
+      testOf('');
+      testOf(10);
+      testOf(0);
+      testOf(true);
+      testOf(false);
+      testOf(() => undefined);
     });
 
     it('throws an error for duplicates', function () {
@@ -99,13 +100,78 @@ describe('IncludeClauseTool', function () {
   });
 
   describe('normalizeClause', function () {
-    it('normalizes a given string', function () {
+    it('throws an error for unsupported values', function () {
+      const throwable = v => () => IncludeClauseTool.normalizeIncludeClause(v);
+      const createError = v =>
+        format(
+          'The provided option "include" should have a non-empty String, ' +
+            'an Object or an Array, but %v given.',
+          v,
+        );
+      const testOf = v => {
+        const error = createError(v);
+        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(throwable(c)).to.throw(error));
+      };
+      testOf('');
+      testOf(10);
+      testOf(0);
+      testOf(true);
+      testOf(false);
+      testOf(() => undefined);
+    });
+
+    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'}])();
+    });
+
+    it('normalizes the 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'});
+    it('normalizes the given key-value object with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: 'bar',
+        baz: 'qux',
+      });
       expect(result).to.be.eql([
         {
           relation: 'foo',
@@ -117,18 +183,12 @@ describe('IncludeClauseTool', function () {
             ],
           },
         },
-      ]);
-    });
-
-    it('normalizes a free-form object with a nested array', function () {
-      const result = IncludeClauseTool.normalizeIncludeClause({foo: ['bar']});
-      expect(result).to.be.eql([
         {
-          relation: 'foo',
+          relation: 'baz',
           scope: {
             include: [
               {
-                relation: 'bar',
+                relation: 'qux',
               },
             ],
           },
@@ -136,9 +196,40 @@ describe('IncludeClauseTool', function () {
       ]);
     });
 
-    it('normalizes a free-form object with a nested free-form object', function () {
+    it('normalizes the given key-value object with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: undefined,
+        baz: undefined,
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+        },
+        {
+          relation: 'baz',
+        },
+      ]);
+    });
+
+    it('normalizes the given key-value object with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: null,
+        baz: null,
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+        },
+        {
+          relation: 'baz',
+        },
+      ]);
+    });
+
+    it('normalizes the given key-value object with a key-value object with string', function () {
       const result = IncludeClauseTool.normalizeIncludeClause({
         foo: {bar: 'baz'},
+        qwe: {asd: 'zxc'},
       });
       expect(result).to.be.eql([
         {
@@ -158,31 +249,16 @@ describe('IncludeClauseTool', function () {
             ],
           },
         },
-      ]);
-    });
-
-    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',
+          relation: 'qwe',
           scope: {
             include: [
               {
-                relation: 'bar',
+                relation: 'asd',
                 scope: {
-                  where: {baz: 'qux'},
                   include: [
                     {
-                      relation: 'baz',
+                      relation: 'zxc',
                     },
                   ],
                 },
@@ -193,27 +269,15 @@ describe('IncludeClauseTool', function () {
       ]);
     });
 
-    it('normalizes an inclusion object', function () {
+    it('normalizes the given key-value object with a key-value object with undefined', function () {
       const result = IncludeClauseTool.normalizeIncludeClause({
-        relation: 'foo',
-        scope: {
-          where: {featured: true},
-          order: 'id',
-          skip: 5,
-          limit: 10,
-          fields: 'id',
-          include: 'bar',
-        },
+        foo: {bar: undefined},
+        qwe: {asd: undefined},
       });
       expect(result).to.be.eql([
         {
           relation: 'foo',
           scope: {
-            where: {featured: true},
-            order: 'id',
-            skip: 5,
-            limit: 10,
-            fields: 'id',
             include: [
               {
                 relation: 'bar',
@@ -221,30 +285,28 @@ describe('IncludeClauseTool', function () {
             ],
           },
         },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+            ],
+          },
+        },
       ]);
     });
 
-    it('normalizes an inclusion object with a nested array', function () {
+    it('normalizes the given key-value object with a key-value object with null', function () {
       const result = IncludeClauseTool.normalizeIncludeClause({
-        relation: 'foo',
-        scope: {
-          where: {featured: true},
-          order: 'id',
-          skip: 5,
-          limit: 10,
-          fields: 'id',
-          include: ['bar'],
-        },
+        foo: {bar: null},
+        qwe: {asd: null},
       });
       expect(result).to.be.eql([
         {
           relation: 'foo',
           scope: {
-            where: {featured: true},
-            order: 'id',
-            skip: 5,
-            limit: 10,
-            fields: 'id',
             include: [
               {
                 relation: 'bar',
@@ -252,40 +314,47 @@ describe('IncludeClauseTool', function () {
             ],
           },
         },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+            ],
+          },
+        },
       ]);
     });
 
-    it('normalizes an inclusion object with a nested free-form object', function () {
+    it('normalizes the given key-value object with an array of strings', function () {
       const result = IncludeClauseTool.normalizeIncludeClause({
-        relation: 'foo',
-        scope: {
-          where: {featured: true},
-          order: 'id',
-          skip: 5,
-          limit: 10,
-          fields: 'id',
-          include: {bar: 'baz'},
-        },
+        foo: ['bar', 'baz'],
+        qwe: ['asd', 'zxc'],
       });
       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',
-                    },
-                  ],
-                },
+              },
+              {
+                relation: 'baz',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+              {
+                relation: 'zxc',
               },
             ],
           },
@@ -293,51 +362,32 @@ describe('IncludeClauseTool', function () {
       ]);
     });
 
-    it('normalizes an inclusion object with a nested inclusion object', function () {
+    it('normalizes the given key-value object with an array of key-value objects with string', 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',
-              },
-            },
-          ],
-        },
+        foo: [{bar: 'baz'}, {qwe: 'asd'}],
+        ewq: [{dsa: 'cxz'}, {rty: 'fgh'}],
       });
       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',
+                      relation: 'baz',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'qwe',
+                scope: {
+                  include: [
+                    {
+                      relation: 'asd',
                     },
                   ],
                 },
@@ -345,26 +395,41 @@ describe('IncludeClauseTool', function () {
             ],
           },
         },
-      ]);
-    });
-
-    it('normalizes an array of strings', function () {
-      const result = IncludeClauseTool.normalizeIncludeClause(['foo', 'bar']);
-      expect(result).to.be.eql([
-        {
-          relation: 'foo',
-        },
         {
-          relation: 'bar',
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+                scope: {
+                  include: [
+                    {
+                      relation: 'cxz',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'rty',
+                scope: {
+                  include: [
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
         },
       ]);
     });
 
-    it('normalizes an array of nested free-form objects', function () {
-      const result = IncludeClauseTool.normalizeIncludeClause([
-        {foo: 'bar'},
-        {baz: 'qux'},
-      ]);
+    it('normalizes the given key-value object with an array of key-value objects with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: [{bar: undefined}, {qwe: undefined}],
+        ewq: [{dsa: undefined}, {rty: undefined}],
+      });
       expect(result).to.be.eql([
         {
           relation: 'foo',
@@ -373,15 +438,21 @@ describe('IncludeClauseTool', function () {
               {
                 relation: 'bar',
               },
+              {
+                relation: 'qwe',
+              },
             ],
           },
         },
         {
-          relation: 'baz',
+          relation: 'ewq',
           scope: {
             include: [
               {
-                relation: 'qux',
+                relation: 'dsa',
+              },
+              {
+                relation: 'rty',
               },
             ],
           },
@@ -389,113 +460,253 @@ describe('IncludeClauseTool', function () {
       ]);
     });
 
-    it('normalizes an array of nested inclusion objects', function () {
-      const result = IncludeClauseTool.normalizeIncludeClause([
+    it('normalizes the given key-value object with an array of key-value objects with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: [{bar: null}, {qwe: null}],
+        ewq: [{dsa: null}, {rty: null}],
+      });
+      expect(result).to.be.eql([
         {
           relation: 'foo',
           scope: {
-            where: {featured: true},
-            order: 'id',
-            skip: 5,
-            limit: 10,
-            fields: 'id',
-            include: 'bar',
+            include: [
+              {
+                relation: 'bar',
+              },
+              {
+                relation: 'qwe',
+              },
+            ],
           },
         },
         {
-          relation: 'baz',
-        },
-      ]);
-      expect(result).to.be.eql([
-        {
-          relation: 'foo',
+          relation: 'ewq',
           scope: {
-            where: {featured: true},
-            order: 'id',
-            skip: 5,
-            limit: 10,
-            fields: 'id',
             include: [
               {
-                relation: 'bar',
+                relation: 'dsa',
+              },
+              {
+                relation: 'rty',
               },
             ],
           },
         },
-        {
-          relation: 'baz',
-        },
       ]);
     });
 
-    it('normalizes an array of nested arrays', function () {
-      const result = IncludeClauseTool.normalizeIncludeClause([
-        ['foo'],
-        ['bar', ['baz']],
-      ]);
+    it('normalizes the given key-value object with an array of key-value objects with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: [{bar: ['baz', 'qux']}, {qwe: ['asd', 'zxc']}],
+        ewq: [{dsa: ['cxz', 'rty']}, {fgh: ['vbn', 'uio']}],
+      });
       expect(result).to.be.eql([
         {
           relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'qux',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'qwe',
+                scope: {
+                  include: [
+                    {
+                      relation: 'asd',
+                    },
+                    {
+                      relation: 'zxc',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
         },
         {
-          relation: 'bar',
-        },
-        {
-          relation: 'baz',
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+                scope: {
+                  include: [
+                    {
+                      relation: 'cxz',
+                    },
+                    {
+                      relation: 'rty',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'fgh',
+                scope: {
+                  include: [
+                    {
+                      relation: 'vbn',
+                    },
+                    {
+                      relation: 'uio',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
         },
       ]);
     });
 
-    it('normalizes a free-form object with mixed inclusions', function () {
+    it('normalizes the given key-value object with an inclusion object with string', 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',
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: 'baz',
+          },
+        },
+        qwe: {
+          relation: 'asd',
           scope: {
-            fields: ['id', 'featured'],
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: 'zxc',
           },
         },
       });
       expect(result).to.be.eql([
-        // a string
         {
           relation: 'foo',
           scope: {
             include: [
               {
                 relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                  ],
+                },
               },
             ],
           },
         },
-        // an array
         {
-          relation: 'baz',
+          relation: 'qwe',
           scope: {
             include: [
               {
-                relation: 'qux',
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                  ],
+                },
               },
             ],
           },
         },
-        // a free-form object
+      ]);
+    });
+
+    it('normalizes the given key-value object with an inclusion object with a key-value object with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: {
+              baz: 'qux',
+              ewq: 'dsa',
+            },
+          },
+        },
+        qwe: {
+          relation: 'asd',
+          scope: {
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: {
+              zxc: 'rty',
+              fgh: 'vbn',
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
         {
-          relation: 'bat',
+          relation: 'foo',
           scope: {
             include: [
               {
-                relation: 'qwe',
+                relation: 'bar',
                 scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
                   include: [
                     {
-                      relation: 'asd',
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                        ],
+                      },
                     },
                   ],
                 },
@@ -503,15 +714,40 @@ describe('IncludeClauseTool', function () {
             ],
           },
         },
-        // an inclusion object
         {
-          relation: 'zxc',
+          relation: 'qwe',
           scope: {
             include: [
               {
-                relation: 'rty',
+                relation: 'asd',
                 scope: {
-                  fields: ['id', 'featured'],
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'rty',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'fgh',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'vbn',
+                          },
+                        ],
+                      },
+                    },
+                  ],
                 },
               },
             ],
@@ -520,134 +756,3998 @@ describe('IncludeClauseTool', function () {
       ]);
     });
 
-    it('normalizes an array with mixed inclusions', function () {
-      const result = IncludeClauseTool.normalizeIncludeClause([
-        // a string
-        'foo',
-        // a free-form object
-        {
-          bar: 'baz',
-          qux: {
-            relation: 'qwe',
+    it('normalizes the given key-value object with an inclusion object with a key-value object with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: {
+              baz: undefined,
+              ewq: undefined,
+            },
           },
         },
-        // an inclusion object
-        {
+        qwe: {
           relation: 'asd',
           scope: {
-            include: 'zxc',
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: {
+              zxc: undefined,
+              fgh: undefined,
+            },
           },
         },
-        // 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: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
               },
             ],
           },
         },
         {
-          relation: 'qux',
+          relation: 'qwe',
           scope: {
             include: [
               {
-                relation: 'qwe',
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
               },
             ],
           },
         },
-        // an inclusion object
-        {
+      ]);
+    });
+
+    it('normalizes the given key-value object with an inclusion object with a key-value object with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: {
+              baz: null,
+              ewq: null,
+            },
+          },
+        },
+        qwe: {
           relation: 'asd',
+          scope: {
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: {
+              zxc: null,
+              fgh: null,
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
           scope: {
             include: [
               {
-                relation: 'zxc',
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
               },
             ],
           },
         },
-        // a nested array
-        {
-          relation: 'rty',
-        },
-        {
-          relation: 'fgh',
-        },
         {
-          relation: 'vbn',
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
         },
       ]);
     });
 
-    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 %v given.',
-          v,
-        );
-      const testFor = v => {
-        const error = createError(v);
-        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);
-      testFor(true);
-      testFor(() => undefined);
+    it('normalizes the given key-value object with an inclusion object with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: ['baz', 'qux'],
+          },
+        },
+        qwe: {
+          relation: 'asd',
+          scope: {
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: ['zxc', 'rty'],
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'qux',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'rty',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given key-value object with an inclusion object with an array of key-value objects with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: {
+              baz: 'qux',
+              ewq: 'dsa',
+            },
+          },
+        },
+        qwe: {
+          relation: 'asd',
+          scope: {
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: {
+              zxc: 'rty',
+              fgh: 'vbn',
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'rty',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'fgh',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'vbn',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given key-value object with an inclusion object with an array of key-value objects with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: {
+              baz: undefined,
+              ewq: undefined,
+            },
+          },
+        },
+        qwe: {
+          relation: 'asd',
+          scope: {
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: {
+              zxc: undefined,
+              fgh: undefined,
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given key-value object with an inclusion object with an array of key-value objects with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: {
+              baz: null,
+              ewq: null,
+            },
+          },
+        },
+        qwe: {
+          relation: 'asd',
+          scope: {
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: {
+              zxc: null,
+              fgh: null,
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given key-value object with an inclusion object with an array of key-value objects with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        foo: {
+          relation: 'bar',
+          scope: {
+            where: {featured: true},
+            order: 'id DESC',
+            skip: 0,
+            limit: 10,
+            fields: 'barId',
+            include: {
+              baz: ['qux', 'xuq'],
+              ewq: ['dsa', 'asd'],
+            },
+          },
+        },
+        qwe: {
+          relation: 'asd',
+          scope: {
+            where: {removed: false},
+            order: 'createdAt DESC',
+            skip: 10,
+            limit: 0,
+            fields: 'asdId',
+            include: {
+              zxc: ['rty', 'ytr'],
+              fgh: ['vbn', 'nbv'],
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                          {
+                            relation: 'xuq',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                          {
+                            relation: 'asd',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'rty',
+                          },
+                          {
+                            relation: 'ytr',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'fgh',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'vbn',
+                          },
+                          {
+                            relation: 'nbv',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with string', 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 the given inclusion object with a key-value object with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: {
+            bar: 'baz',
+            qwe: 'asd',
+          },
+        },
+      });
+      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',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'qwe',
+                scope: {
+                  include: [
+                    {
+                      relation: 'asd',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with a key-value object with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: {
+            bar: undefined,
+            qwe: undefined,
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+              },
+              {
+                relation: 'qwe',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with a key-value object with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: {
+            bar: null,
+            qwe: null,
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+              },
+              {
+                relation: 'qwe',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an array of strings', 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',
+              },
+              {
+                relation: 'baz',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an array of key-value objects with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: [{bar: 'baz'}, {qwe: 'asd'}],
+        },
+      });
+      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',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'qwe',
+                scope: {
+                  include: [
+                    {
+                      relation: 'asd',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an array of key-value objects with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: [{bar: undefined}, {qwe: undefined}],
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+              },
+              {
+                relation: 'qwe',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an array of key-value objects with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: [{bar: null}, {qwe: null}],
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'id',
+            include: [
+              {
+                relation: 'bar',
+              },
+              {
+                relation: 'qwe',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an array of key-value objects with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'id',
+          include: [{bar: ['baz', 'qux']}, {qwe: ['asd', 'zxc']}],
+        },
+      });
+      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',
+                    },
+                    {
+                      relation: 'qux',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'qwe',
+                scope: {
+                  include: [
+                    {
+                      relation: 'asd',
+                    },
+                    {
+                      relation: 'zxc',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: 'baz',
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with a key-value object with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: {
+                baz: 'qux',
+                ewq: 'dsa',
+              },
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with a key-value object with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: {
+                baz: undefined,
+                ewq: undefined,
+              },
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with a key-value object with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: {
+                baz: null,
+                ewq: null,
+              },
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: ['baz', 'qux'],
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'qux',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with an array of key-value objects with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: [{baz: 'qux'}, {ewq: 'dsa'}],
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with an array of key-value objects with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: [{baz: undefined}, {ewq: undefined}],
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with an array of key-value objects with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: [{baz: null}, {ewq: null}],
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given inclusion object with an inclusion object with an array of key-value objects with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause({
+        relation: 'foo',
+        scope: {
+          where: {featured: true},
+          order: 'id',
+          skip: 5,
+          limit: 10,
+          fields: 'fooId',
+          include: {
+            relation: 'bar',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 0,
+              limit: 0,
+              fields: ['id', 'createdAt'],
+              include: [{baz: ['qux', 'xuq']}, {ewq: ['dsa', 'asd']}],
+            },
+          },
+        },
+      });
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            where: {featured: true},
+            order: 'id',
+            skip: 5,
+            limit: 10,
+            fields: 'fooId',
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 0,
+                  limit: 0,
+                  fields: ['id', 'createdAt'],
+                  include: [
+                    {
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                          {
+                            relation: 'xuq',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                          {
+                            relation: 'asd',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause(['foo', 'bar']);
+      expect(result).to.be.eql([{relation: 'foo'}, {relation: 'bar'}]);
+    });
+
+    it('normalizes the given array of key-value objects with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {foo: 'bar', baz: 'qux'},
+        {qwe: 'asd', zxc: 'rty'},
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'baz',
+          scope: {
+            include: [
+              {
+                relation: 'qux',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'zxc',
+          scope: {
+            include: [
+              {
+                relation: 'rty',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {foo: undefined, bar: undefined},
+        {baz: undefined, qux: undefined},
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+        },
+        {
+          relation: 'bar',
+        },
+        {
+          relation: 'baz',
+        },
+        {
+          relation: 'qux',
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {foo: null, bar: null},
+        {baz: null, qux: null},
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+        },
+        {
+          relation: 'bar',
+        },
+        {
+          relation: 'baz',
+        },
+        {
+          relation: 'qux',
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with a key-value object with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {bar: 'baz'},
+          qwe: {asd: 'zxc'},
+        },
+        {
+          ewq: {dsa: 'cxz'},
+          rty: {fgh: 'vbn'},
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+                scope: {
+                  include: [
+                    {
+                      relation: 'cxz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'rty',
+          scope: {
+            include: [
+              {
+                relation: 'fgh',
+                scope: {
+                  include: [
+                    {
+                      relation: 'vbn',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with a key-value object with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {bar: undefined},
+          qwe: {asd: undefined},
+        },
+        {
+          ewq: {dsa: undefined},
+          cxz: {rty: undefined},
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'cxz',
+          scope: {
+            include: [
+              {
+                relation: 'rty',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with a key-value object with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {bar: null},
+          qwe: {asd: null},
+        },
+        {
+          ewq: {dsa: null},
+          cxz: {rty: null},
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'cxz',
+          scope: {
+            include: [
+              {
+                relation: 'rty',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: ['bar', 'baz'],
+          qwe: ['asd', 'zxc'],
+        },
+        {
+          ewq: ['dsa', 'cxz'],
+          rty: ['fgh', 'vbn'],
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+              {
+                relation: 'baz',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+              {
+                relation: 'zxc',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+              },
+              {
+                relation: 'cxz',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'rty',
+          scope: {
+            include: [
+              {
+                relation: 'fgh',
+              },
+              {
+                relation: 'vbn',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an array of key-value objects with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: [{bar: 'baz'}, {qwe: 'asd'}],
+          ewq: [{dsa: 'cxz'}, {rty: 'fgh'}],
+        },
+        {
+          qwe: [{asd: 'zxc'}, {rty: 'fgh'}],
+          vbn: [{ewq: 'dsa'}, {cxz: 'rty'}],
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'qwe',
+                scope: {
+                  include: [
+                    {
+                      relation: 'asd',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+                scope: {
+                  include: [
+                    {
+                      relation: 'cxz',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'rty',
+                scope: {
+                  include: [
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'rty',
+                scope: {
+                  include: [
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'vbn',
+          scope: {
+            include: [
+              {
+                relation: 'ewq',
+                scope: {
+                  include: [
+                    {
+                      relation: 'dsa',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'cxz',
+                scope: {
+                  include: [
+                    {
+                      relation: 'rty',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an array of key-value objects with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: [{bar: undefined}, {qwe: undefined}],
+          ewq: [{dsa: undefined}, {rty: undefined}],
+        },
+        {
+          qwe: [{asd: undefined}, {zxc: undefined}],
+          rty: [{fgh: undefined}, {vbn: undefined}],
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+              {
+                relation: 'qwe',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+              },
+              {
+                relation: 'rty',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+              {
+                relation: 'zxc',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'rty',
+          scope: {
+            include: [
+              {
+                relation: 'fgh',
+              },
+              {
+                relation: 'vbn',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an array of key-value objects with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: [{bar: null}, {qwe: null}],
+          ewq: [{dsa: null}, {rty: null}],
+        },
+        {
+          ytr: [{hgf: null}, {nbv: null}],
+          qwe: [{asd: null}, {zxc: null}],
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+              },
+              {
+                relation: 'qwe',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+              },
+              {
+                relation: 'rty',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+              },
+              {
+                relation: 'nbv',
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+              },
+              {
+                relation: 'zxc',
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an array of key-value objects with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: [{bar: ['baz', 'qux']}, {qwe: ['asd', 'zxc']}],
+          ewq: [{dsa: ['cxz', 'rty']}, {fgh: ['vbn', 'uio']}],
+        },
+        {
+          qwe: [{asd: ['zxc', 'rty']}, {fgh: ['vbn', 'ewq']}],
+          dsa: [{cxz: ['rty', 'vbn']}, {ewq: ['dsa', 'cxz']}],
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'qux',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'qwe',
+                scope: {
+                  include: [
+                    {
+                      relation: 'asd',
+                    },
+                    {
+                      relation: 'zxc',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+                scope: {
+                  include: [
+                    {
+                      relation: 'cxz',
+                    },
+                    {
+                      relation: 'rty',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'fgh',
+                scope: {
+                  include: [
+                    {
+                      relation: 'vbn',
+                    },
+                    {
+                      relation: 'uio',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'rty',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'fgh',
+                scope: {
+                  include: [
+                    {
+                      relation: 'vbn',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'dsa',
+          scope: {
+            include: [
+              {
+                relation: 'cxz',
+                scope: {
+                  include: [
+                    {
+                      relation: 'rty',
+                    },
+                    {
+                      relation: 'vbn',
+                    },
+                  ],
+                },
+              },
+              {
+                relation: 'ewq',
+                scope: {
+                  include: [
+                    {
+                      relation: 'dsa',
+                    },
+                    {
+                      relation: 'cxz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an inclusion object with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: 'baz',
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: 'zxc',
+            },
+          },
+        },
+        {
+          ewq: {
+            relation: 'dsa',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: 'cxz',
+            },
+          },
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: 'nbv',
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'cxz',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an inclusion object with a key-value object with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                baz: 'qux',
+                ewq: 'dsa',
+              },
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                zxc: 'rty',
+                fgh: 'vbn',
+              },
+            },
+          },
+        },
+        {
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                nbv: 'ewq',
+                dsa: 'cxz',
+              },
+            },
+          },
+          rty: {
+            relation: 'fgh',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                vbn: 'qwe',
+                asd: 'zxc',
+              },
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'rty',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'fgh',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'vbn',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'ewq',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'dsa',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'cxz',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'rty',
+          scope: {
+            include: [
+              {
+                relation: 'fgh',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'vbn',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qwe',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'asd',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'zxc',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an inclusion object with a key-value object with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                baz: undefined,
+                ewq: undefined,
+              },
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                zxc: undefined,
+                fgh: undefined,
+              },
+            },
+          },
+        },
+        {
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                nbv: undefined,
+                ewq: undefined,
+              },
+            },
+          },
+          dsa: {
+            relation: 'cxz',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                rty: undefined,
+                fgh: undefined,
+              },
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'dsa',
+          scope: {
+            include: [
+              {
+                relation: 'cxz',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'rty',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an inclusion object with a key-value object with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                baz: null,
+                ewq: null,
+              },
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                zxc: null,
+                fgh: null,
+              },
+            },
+          },
+        },
+        {
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                nbv: null,
+                ewq: null,
+              },
+            },
+          },
+          dsa: {
+            relation: 'cxz',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                rty: null,
+                fgh: null,
+              },
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'dsa',
+          scope: {
+            include: [
+              {
+                relation: 'cxz',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'rty',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an inclusion object with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: ['baz', 'qux'],
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: ['zxc', 'rty'],
+            },
+          },
+        },
+        {
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: ['nbv', 'ewq'],
+            },
+          },
+          dsa: {
+            relation: 'cxz',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: ['ytr', 'hgf'],
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'qux',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'rty',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'dsa',
+          scope: {
+            include: [
+              {
+                relation: 'cxz',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'ytr',
+                    },
+                    {
+                      relation: 'hgf',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an inclusion object with an array of key-value objects with string', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                baz: 'qux',
+                ewq: 'dsa',
+              },
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                zxc: 'rty',
+                fgh: 'vbn',
+              },
+            },
+          },
+        },
+        {
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                nbv: 'ewq',
+                dsa: 'cxz',
+              },
+            },
+          },
+          rty: {
+            relation: 'fgh',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                vbn: 'qwe',
+                asd: 'zxc',
+              },
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'rty',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'fgh',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'vbn',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'ewq',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'dsa',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'cxz',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'rty',
+          scope: {
+            include: [
+              {
+                relation: 'fgh',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'vbn',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qwe',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'asd',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'zxc',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an inclusion object with an array of key-value objects with undefined', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                baz: undefined,
+                ewq: undefined,
+              },
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                zxc: undefined,
+                fgh: undefined,
+              },
+            },
+          },
+        },
+        {
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                nbv: undefined,
+                ewq: undefined,
+              },
+            },
+          },
+          dsa: {
+            relation: 'cxz',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                ytr: undefined,
+                hgf: undefined,
+              },
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'dsa',
+          scope: {
+            include: [
+              {
+                relation: 'cxz',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'ytr',
+                    },
+                    {
+                      relation: 'hgf',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
     });
 
-    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'}])();
+    it('normalizes the given array of key-value objects with an inclusion object with an array of key-value objects with null', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                baz: null,
+                ewq: null,
+              },
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                zxc: null,
+                fgh: null,
+              },
+            },
+          },
+        },
+        {
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                nbv: null,
+                ewq: null,
+              },
+            },
+          },
+          dsa: {
+            relation: 'cxz',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                ytr: null,
+                hgf: null,
+              },
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                    },
+                    {
+                      relation: 'fgh',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                    },
+                    {
+                      relation: 'ewq',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'dsa',
+          scope: {
+            include: [
+              {
+                relation: 'cxz',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'ytr',
+                    },
+                    {
+                      relation: 'hgf',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
+    });
+
+    it('normalizes the given array of key-value objects with an inclusion object with an array of key-value objects with an array of strings', function () {
+      const result = IncludeClauseTool.normalizeIncludeClause([
+        {
+          foo: {
+            relation: 'bar',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                baz: ['qux', 'xuq'],
+                ewq: ['dsa', 'asd'],
+              },
+            },
+          },
+          qwe: {
+            relation: 'asd',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                zxc: ['rty', 'ytr'],
+                fgh: ['vbn', 'nbv'],
+              },
+            },
+          },
+        },
+        {
+          ytr: {
+            relation: 'hgf',
+            scope: {
+              where: {featured: true},
+              order: 'id DESC',
+              skip: 0,
+              limit: 10,
+              fields: 'barId',
+              include: {
+                nbv: ['ewq', 'dsa'],
+                cxz: ['rty', 'fgh'],
+              },
+            },
+          },
+          ewq: {
+            relation: 'dsa',
+            scope: {
+              where: {removed: false},
+              order: 'createdAt DESC',
+              skip: 10,
+              limit: 0,
+              fields: 'asdId',
+              include: {
+                cxz: ['ytr', 'hgf'],
+                nbv: ['qwe', 'asd'],
+              },
+            },
+          },
+        },
+      ]);
+      expect(result).to.be.eql([
+        {
+          relation: 'foo',
+          scope: {
+            include: [
+              {
+                relation: 'bar',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'baz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qux',
+                          },
+                          {
+                            relation: 'xuq',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'ewq',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'dsa',
+                          },
+                          {
+                            relation: 'asd',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'qwe',
+          scope: {
+            include: [
+              {
+                relation: 'asd',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'zxc',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'rty',
+                          },
+                          {
+                            relation: 'ytr',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'fgh',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'vbn',
+                          },
+                          {
+                            relation: 'nbv',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ytr',
+          scope: {
+            include: [
+              {
+                relation: 'hgf',
+                scope: {
+                  where: {featured: true},
+                  order: 'id DESC',
+                  skip: 0,
+                  limit: 10,
+                  fields: 'barId',
+                  include: [
+                    {
+                      relation: 'nbv',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'ewq',
+                          },
+                          {
+                            relation: 'dsa',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'cxz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'rty',
+                          },
+                          {
+                            relation: 'fgh',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        {
+          relation: 'ewq',
+          scope: {
+            include: [
+              {
+                relation: 'dsa',
+                scope: {
+                  where: {removed: false},
+                  order: 'createdAt DESC',
+                  skip: 10,
+                  limit: 0,
+                  fields: 'asdId',
+                  include: [
+                    {
+                      relation: 'cxz',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'ytr',
+                          },
+                          {
+                            relation: 'hgf',
+                          },
+                        ],
+                      },
+                    },
+                    {
+                      relation: 'nbv',
+                      scope: {
+                        include: [
+                          {
+                            relation: 'qwe',
+                          },
+                          {
+                            relation: 'asd',
+                          },
+                        ],
+                      },
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      ]);
     });
   });
 });