Browse Source

fix: filtering by the "order" option

e22m4u 2 years ago
parent
commit
5161b47607
2 changed files with 652 additions and 397 deletions
  1. 20 17
      src/filter/order-clause-tool.js
  2. 632 380
      src/filter/order-clause-tool.spec.js

+ 20 - 17
src/filter/order-clause-tool.js

@@ -13,14 +13,15 @@ export class OrderClauseTool extends Service {
    * @param {string|string[]|undefined} clause
    */
   sort(entities, clause) {
-    if (!clause) return;
-    if (!Array.isArray(clause)) clause = [clause];
+    if (clause == null) return;
+    if (Array.isArray(clause) === false) clause = [clause];
+    if (!clause.length) return;
     const mapping = [];
     clause.forEach((key, index) => {
-      if (typeof key !== 'string')
+      if (!key || typeof key !== 'string')
         throw new InvalidArgumentError(
-          'The provided option "order" should be a String ' +
-            'or an Array of String, but %v given.',
+          'The provided option "order" should be a non-empty String ' +
+            'or an Array of non-empty String, but %v given.',
           key,
         );
       let reverse = 1;
@@ -40,14 +41,15 @@ export class OrderClauseTool extends Service {
    * @param {string|string[]|undefined} clause
    */
   static validateOrderClause(clause) {
-    if (!clause) return;
-    const tempClause = Array.isArray(clause) ? clause : [clause];
-    tempClause.forEach(key => {
-      if (!key || typeof key !== 'string')
+    if (clause == null) return;
+    if (Array.isArray(clause) === false) clause = [clause];
+    if (!clause.length) return;
+    clause.forEach(field => {
+      if (!field || typeof field !== 'string')
         throw new InvalidArgumentError(
           'The provided option "order" should be a non-empty String ' +
-            'or an Array of String, but %v given.',
-          key,
+            'or an Array of non-empty String, but %v given.',
+          field,
         );
     });
   }
@@ -59,14 +61,15 @@ export class OrderClauseTool extends Service {
    * @returns {string[]|undefined}
    */
   static normalizeOrderClause(clause) {
-    if (!clause) return;
-    clause = Array.isArray(clause) ? clause : [clause];
-    clause.forEach(key => {
-      if (!key || typeof key !== 'string')
+    if (clause == null) return;
+    if (Array.isArray(clause) === false) clause = [clause];
+    if (!clause.length) return;
+    clause.forEach(field => {
+      if (!field || typeof field !== 'string')
         throw new InvalidArgumentError(
           'The provided option "order" should be a non-empty String ' +
-            'or an Array of String, but %v given.',
-          key,
+            'or an Array of non-empty String, but %v given.',
+          field,
         );
     });
     return clause;

+ 632 - 380
src/filter/order-clause-tool.spec.js

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