From 0dcdc190216e1ca45cb01d749185b4b5d99720e3 Mon Sep 17 00:00:00 2001
From: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
Date: Sat, 16 Apr 2022 09:00:45 +0600
Subject: [PATCH] add gt lt query (#1415)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
---
 packages/core/src/__tests__/memdb.test.ts | 92 ++++++++++++++++++-----
 packages/core/src/predicate.ts            | 21 +++++-
 packages/core/src/storage.ts              | 12 ++-
 3 files changed, 104 insertions(+), 21 deletions(-)

diff --git a/packages/core/src/__tests__/memdb.test.ts b/packages/core/src/__tests__/memdb.test.ts
index 1a05ba8473..954f13b59a 100644
--- a/packages/core/src/__tests__/memdb.test.ts
+++ b/packages/core/src/__tests__/memdb.test.ts
@@ -36,13 +36,15 @@ class ClientModel extends ModelDb implements Client {
     return this
   }
 
-  async findOne<T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<WithLookup<T> | undefined> {
+  async findOne<T extends Doc>(
+    _class: Ref<Class<T>>,
+    query: DocumentQuery<T>,
+    options?: FindOptions<T>
+  ): Promise<WithLookup<T> | undefined> {
     return (await this.findAll(_class, query, options)).shift()
   }
 
-  async close (): Promise<void> {
-
-  }
+  async close (): Promise<void> {}
 }
 
 async function createModel (): Promise<{ model: ClientModel, hierarchy: Hierarchy, txDb: TxDb }> {
@@ -84,7 +86,13 @@ describe('memdb', () => {
     const result2 = await client.findAll(core.class.Space, {})
     expect(result2).toHaveLength(3)
 
-    await client.createDoc(core.class.Space, core.space.Model, { private: false, name: 'NewSpace', description: '', members: [], archived: false })
+    await client.createDoc(core.class.Space, core.space.Model, {
+      private: false,
+      name: 'NewSpace',
+      description: '',
+      members: [],
+      archived: false
+    })
     const result3 = await client.findAll(core.class.Space, {})
     expect(result3).toHaveLength(4)
   })
@@ -92,7 +100,7 @@ describe('memdb', () => {
   it('should query model', async () => {
     const { model } = await createModel()
     const result = await model.findAll(core.class.Class, {})
-    const names = result.map(d => d._id)
+    const names = result.map((d) => d._id)
     expect(names.includes(core.class.Class)).toBe(true)
     const result2 = await model.findAll(core.class.Class, { _id: undefined })
     expect(result2.length).toBe(0)
@@ -108,7 +116,9 @@ describe('memdb', () => {
     const { model } = await createModel()
     const ops = new TxOperations(model, core.account.System)
 
-    await ops.createMixin<Doc, TestMixin>(core.class.Obj, core.class.Class, core.space.Model, test.mixin.TestMixin, { arr: ['hello'] })
+    await ops.createMixin<Doc, TestMixin>(core.class.Obj, core.class.Class, core.space.Model, test.mixin.TestMixin, {
+      arr: ['hello']
+    })
     const objClass = (await model.findAll(core.class.Class, { _id: core.class.Obj }))[0] as any
     expect(objClass['test:mixin:TestMixin'].arr).toEqual(expect.arrayContaining(['hello']))
 
@@ -160,6 +170,24 @@ describe('memdb', () => {
       space: { $in: [core.space.Model, core.space.Tx] }
     })
     expect(multipleParam.length).toBeGreaterThan(5)
+
+    const classes = await model.findAll(core.class.Class, {})
+    const gt = await model.findAll(core.class.Class, {
+      kind: { $gt: 1 }
+    })
+    expect(gt.length).toBe(classes.filter((p) => p.kind > 1).length)
+    const gte = await model.findAll(core.class.Class, {
+      kind: { $gte: 1 }
+    })
+    expect(gte.length).toBe(classes.filter((p) => p.kind >= 1).length)
+    const lt = await model.findAll(core.class.Class, {
+      kind: { $lt: 1 }
+    })
+    expect(lt.length).toBe(classes.filter((p) => p.kind < 1).length)
+    const lte = await model.findAll(core.class.Class, {
+      kind: { $lt: 1 }
+    })
+    expect(lte.length).toBe(classes.filter((p) => p.kind <= 1).length)
   })
 
   it('should query model like params', async () => {
@@ -260,25 +288,51 @@ describe('memdb', () => {
     const spaces = await client.findAll(core.class.Space, {})
     expect(spaces).toHaveLength(2)
 
-    const first = await client.addCollection(test.class.TestComment, core.space.Model, spaces[0]._id, spaces[0]._class, 'comments', {
-      message: 'msg'
-    })
+    const first = await client.addCollection(
+      test.class.TestComment,
+      core.space.Model,
+      spaces[0]._id,
+      spaces[0]._class,
+      'comments',
+      {
+        message: 'msg'
+      }
+    )
 
-    const second = await client.addCollection(test.class.TestComment, core.space.Model, first, test.class.TestComment, 'comments', {
-      message: 'msg2'
-    })
+    const second = await client.addCollection(
+      test.class.TestComment,
+      core.space.Model,
+      first,
+      test.class.TestComment,
+      'comments',
+      {
+        message: 'msg2'
+      }
+    )
 
     await client.addCollection(test.class.TestComment, core.space.Model, spaces[0]._id, spaces[0]._class, 'comments', {
       message: 'msg3'
     })
 
-    const simple = await client.findAll(test.class.TestComment, { _id: first }, { lookup: { attachedTo: spaces[0]._class } })
+    const simple = await client.findAll(
+      test.class.TestComment,
+      { _id: first },
+      { lookup: { attachedTo: spaces[0]._class } }
+    )
     expect(simple[0].$lookup?.attachedTo).toEqual(spaces[0])
 
-    const nested = await client.findAll(test.class.TestComment, { _id: second }, { lookup: { attachedTo: [test.class.TestComment, { attachedTo: spaces[0]._class } as any] } })
+    const nested = await client.findAll(
+      test.class.TestComment,
+      { _id: second },
+      { lookup: { attachedTo: [test.class.TestComment, { attachedTo: spaces[0]._class } as any] } }
+    )
     expect((nested[0].$lookup?.attachedTo as any).$lookup?.attachedTo).toEqual(spaces[0])
 
-    const reverse = await client.findAll(spaces[0]._class, { _id: spaces[0]._id }, { lookup: { _id: { comments: test.class.TestComment } } })
+    const reverse = await client.findAll(
+      spaces[0]._class,
+      { _id: spaces[0]._id },
+      { lookup: { _id: { comments: test.class.TestComment } } }
+    )
     expect((reverse[0].$lookup as any).comments).toHaveLength(2)
   })
 
@@ -306,7 +360,11 @@ describe('memdb', () => {
       text: 'qwe2'
     })
 
-    const results = await client.findAll(test.class.TestMixinTodo, {}, { lookup: { attachedTo: test.mixin.TaskMixinTodos } })
+    const results = await client.findAll(
+      test.class.TestMixinTodo,
+      {},
+      { lookup: { attachedTo: test.mixin.TaskMixinTodos } }
+    )
     expect(results.length).toEqual(2)
     const attached = results[0].$lookup?.attachedTo
     expect(attached).toBeDefined()
diff --git a/packages/core/src/predicate.ts b/packages/core/src/predicate.ts
index 41bd8f5a10..aa360664c1 100644
--- a/packages/core/src/predicate.ts
+++ b/packages/core/src/predicate.ts
@@ -49,7 +49,10 @@ const predicates: Record<string, PredicateFactory> = {
   },
 
   $like: (query: string, propertyKey: string): Predicate => {
-    const searchString = query.split('%').map(it => escapeLikeForRegexp(it)).join('.*')
+    const searchString = query
+      .split('%')
+      .map((it) => escapeLikeForRegexp(it))
+      .join('.*')
     const regex = RegExp(`^${searchString}$`, 'i')
 
     return (docs) => execPredicate(docs, propertyKey, (value) => regex.test(value))
@@ -58,11 +61,25 @@ const predicates: Record<string, PredicateFactory> = {
   $regex: (o: { $regex: string, $options: string }, propertyKey: string): Predicate => {
     const re = new RegExp(o.$regex, o.$options)
     return (docs) => execPredicate(docs, propertyKey, (value) => value.match(re) !== null)
+  },
+  $gt: (o, propertyKey) => {
+    return (docs) => execPredicate(docs, propertyKey, (value) => value > o)
+  },
+  $gte: (o, propertyKey) => {
+    return (docs) => execPredicate(docs, propertyKey, (value) => value >= o)
+  },
+  $lt: (o, propertyKey) => {
+    return (docs) => execPredicate(docs, propertyKey, (value) => value < o)
+  },
+  $lte: (o, propertyKey) => {
+    return (docs) => execPredicate(docs, propertyKey, (value) => value <= o)
   }
 }
 
 export function isPredicate (o: Record<string, any>): boolean {
-  if (o === null || typeof o !== 'object') { return false }
+  if (o === null || typeof o !== 'object') {
+    return false
+  }
   const keys = Object.keys(o)
   return keys.length > 0 && keys.every((key) => key.startsWith('$'))
 }
diff --git a/packages/core/src/storage.ts b/packages/core/src/storage.ts
index 58ea8d17a8..6237a34b0f 100644
--- a/packages/core/src/storage.ts
+++ b/packages/core/src/storage.ts
@@ -24,6 +24,10 @@ import type { Tx } from './tx'
 export type QuerySelector<T> = {
   $in?: T[]
   $nin?: T[]
+  $gt?: T extends number ? number : never
+  $gte?: T extends number ? number : never
+  $lt?: T extends number ? number : never
+  $lte?: T extends number ? number : never
   $like?: string
   $regex?: string
   $options?: string
@@ -49,11 +53,15 @@ export type DocumentQuery<T extends Doc> = {
 /**
  * @public
  */
-export type ToClassRefT<T extends object, P extends keyof T> = T[P] extends Ref<infer X> | null | undefined ? Ref<Class<X>> | [Ref<Class<X>>, Lookup<X>] : never
+export type ToClassRefT<T extends object, P extends keyof T> = T[P] extends Ref<infer X> | null | undefined
+  ? Ref<Class<X>> | [Ref<Class<X>>, Lookup<X>]
+  : never
 /**
  * @public
  */
-export type ToClassRefTA<T extends object, P extends keyof T> = T[P] extends Array<Ref<infer X>> | null | undefined ? Ref<Class<X>> | [Ref<Class<X>>, Lookup<X>] : never
+export type ToClassRefTA<T extends object, P extends keyof T> = T[P] extends Array<Ref<infer X>> | null | undefined
+  ? Ref<Class<X>> | [Ref<Class<X>>, Lookup<X>]
+  : never
 /**
  * @public
  */