From 687e3c7c36904221b2707d0220c0893e3cb1faa9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michael=20Hillerstr=C3=B6m?= <michael@hillerstrom.name>
Date: Wed, 31 Mar 2021 07:23:56 +0200
Subject: [PATCH] feat(adapter-commons): Added mongoDB like search in embedded
 objects

---
 packages/adapter-commons/src/sort.ts       | 29 ++++++++--
 packages/adapter-commons/test/sort.test.ts | 65 ++++++++++++++++++++++
 2 files changed, 89 insertions(+), 5 deletions(-)

diff --git a/packages/adapter-commons/src/sort.ts b/packages/adapter-commons/src/sort.ts
index 5803a000c3..d83dcd6880 100644
--- a/packages/adapter-commons/src/sort.ts
+++ b/packages/adapter-commons/src/sort.ts
@@ -69,17 +69,36 @@ export function compare (a: any, b: any, compareStrings: any = exports.compareNS
 // An in-memory sorting function according to the
 // $sort special query parameter
 export function sorter ($sort: any) {
-  const criteria = Object.keys($sort).map(key => {
-    const direction = $sort[key];
+  let sortLevels = false; // True if $sort has tags with '.' i.e. '{a: 1, b: -1, "c.x.z": 1}'
+
+  const getVal = (a: any, sortKeys: any[]) => {
+    let keys = sortKeys.map(key => key);
+    let val = a;
+    do {
+      let key = keys.shift();
+      val = val[key];
+    } while (keys.length);
+  
+    return val;
+  };
+  
+    const criteria = Object.keys($sort).map(key => {
+      const direction = $sort[key];
+      const keys = key.split('.');
+      sortLevels = keys.length > 1;
 
-    return { key, direction };
-  });
+      return { keys, direction };
+    });
 
   return function (a: any, b: any) {
     let compare;
 
     for (const criterion of criteria) {
-      compare = criterion.direction * exports.compare(a[criterion.key], b[criterion.key]);
+    if (sortLevels) {
+      compare = criterion.direction * exports.compare(getVal(a, criterion.keys), getVal(b, criterion.keys));
+    } else {
+      compare = criterion.direction * exports.compare(a[criterion.keys[0]], b[criterion.keys[0]]);
+    }
 
       if (compare !== 0) {
         return compare;
diff --git a/packages/adapter-commons/test/sort.test.ts b/packages/adapter-commons/test/sort.test.ts
index 88e144b56c..1dde313091 100644
--- a/packages/adapter-commons/test/sort.test.ts
+++ b/packages/adapter-commons/test/sort.test.ts
@@ -176,4 +176,69 @@ describe('@feathersjs/adapter-commons', () => {
       ]);
     });
   });
+
+  describe('sorter mongoDB-like sorting on embedded objects', () => {
+    let data: any[] = [];
+
+    beforeEach(() => {
+      data = [
+        { _id: 1, item: { category: "cake", type: "chiffon" }, amount: 10 },
+        { _id: 2, item: { category: "cookies", type: "chocolate chip" }, amount: 50 },
+        { _id: 3, item: { category: "cookies", type: "chocolate chip" }, amount: 15 },
+        { _id: 4, item: { category: "cake", type: "lemon" }, amount: 30 },
+        { _id: 5, item: { category: "cake", type: "carrot" }, amount: 20 },
+        { _id: 6, item: { category: "brownies", type: "blondie" }, amount: 10 }
+      ];
+
+    });
+
+    it('straight test', () => {
+      const sort = sorter({
+        amount: -1
+      });
+
+      assert.deepStrictEqual(data.sort(sort), [
+        { _id: 2, item: { category: "cookies", type: "chocolate chip" }, amount: 50 },
+        { _id: 4, item: { category: "cake", type: "lemon" }, amount: 30 },
+        { _id: 5, item: { category: "cake", type: "carrot" }, amount: 20 },
+        { _id: 3, item: { category: "cookies", type: "chocolate chip" }, amount: 15 },
+        { _id: 1, item: { category: "cake", type: "chiffon" }, amount: 10 },
+        { _id: 6, item: { category: "brownies", type: "blondie" }, amount: 10 }
+      ]);
+    });
+
+    it('embedded sort 1', () => {
+      const sort = sorter({
+        "item.category": 1,
+        "item.type": 1,
+      });
+
+      assert.deepStrictEqual(data.sort(sort), [
+        { _id: 6, item: { category: "brownies", type: "blondie" }, amount: 10 },
+        { _id: 5, item: { category: "cake", type: "carrot" }, amount: 20 },
+        { _id: 1, item: { category: "cake", type: "chiffon" }, amount: 10 },
+        { _id: 4, item: { category: "cake", type: "lemon" }, amount: 30 },
+        { _id: 2, item: { category: "cookies", type: "chocolate chip" }, amount: 50 },
+        { _id: 3, item: { category: "cookies", type: "chocolate chip" }, amount: 15 }
+      ]);
+    });
+
+    it('embedded sort 2', () => {
+      const sort = sorter({
+        "item.category": 1,
+        "item.type": 1,
+        amount: 1
+      });
+
+      assert.deepStrictEqual(data.sort(sort), [
+        { _id: 6, item: { category: "brownies", type: "blondie" }, amount: 10 },
+        { _id: 5, item: { category: "cake", type: "carrot" }, amount: 20 },
+        { _id: 1, item: { category: "cake", type: "chiffon" }, amount: 10 },
+        { _id: 4, item: { category: "cake", type: "lemon" }, amount: 30 },
+        { _id: 3, item: { category: "cookies", type: "chocolate chip" }, amount: 15 },
+        { _id: 2, item: { category: "cookies", type: "chocolate chip" }, amount: 50 }
+      ]);
+    });
+ });
+
 });