From e11a0a87cbf74735ab1c30ef3006c28e8d38444c Mon Sep 17 00:00:00 2001
From: Andrey Sobolev <haiodo@users.noreply.github.com>
Date: Sat, 26 Oct 2024 21:17:37 +0700
Subject: [PATCH] UBERF-8540: Allow derived operations with apply (#7044)

---
 models/notification/src/migration.ts          | 12 +++++++
 packages/core/src/operations.ts               |  4 +--
 packages/core/src/server.ts                   |  2 ++
 .../src/components/ChannelScrollView.svelte   |  2 +-
 .../ReverseChannelScrollView.svelte           |  2 +-
 .../src/components/chat/utils.ts              |  2 +-
 .../src/inboxNotificationsClient.ts           |  6 ++--
 plugins/notification-resources/src/utils.ts   |  4 +--
 .../components/WorkbenchTabPresenter.svelte   | 16 +++++----
 server/core/src/mem.ts                        |  2 ++
 server/core/src/plugin.ts                     |  3 +-
 server/elastic/src/backup.ts                  |  4 +++
 server/middleware/src/lowLevel.ts             | 35 +++++++++----------
 server/mongo/src/storage.ts                   |  4 +++
 server/postgres/src/storage.ts                |  7 ++++
 server/server-storage/src/blobStorage.ts      |  2 ++
 server/tool/src/upgrade.ts                    |  8 +----
 17 files changed, 71 insertions(+), 44 deletions(-)

diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts
index 4c67119729..19405eaf68 100644
--- a/models/notification/src/migration.ts
+++ b/models/notification/src/migration.ts
@@ -399,6 +399,18 @@ export const notificationOperation: MigrateOperation = {
         func: async (client: MigrationClient): Promise<void> => {
           await client.update(DOMAIN_DOC_NOTIFY, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } })
         }
+      },
+      {
+        state: 'remove-update-txes-docnotify-ctx',
+        func: async (client) => {
+          await client.deleteMany(DOMAIN_TX, {
+            _class: core.class.TxUpdateDoc,
+            objectClass: notification.class.DocNotifyContext,
+            'operations.lastViewedTimestamp': {
+              $exists: true
+            }
+          })
+        }
       }
     ])
 
diff --git a/packages/core/src/operations.ts b/packages/core/src/operations.ts
index 8d55f8ec82..c8d40c1c62 100644
--- a/packages/core/src/operations.ts
+++ b/packages/core/src/operations.ts
@@ -313,8 +313,8 @@ export class TxOperations implements Omit<Client, 'notify'> {
     return this.removeDoc(doc._class, doc.space, doc._id)
   }
 
-  apply (scope?: string, measure?: string): ApplyOperations {
-    return new ApplyOperations(this, scope, measure, this.isDerived)
+  apply (scope?: string, measure?: string, derived?: boolean): ApplyOperations {
+    return new ApplyOperations(this, scope, measure, derived ?? this.isDerived)
   }
 
   async diffUpdate<T extends Doc = Doc>(
diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts
index 8ae4596a6e..5a16bfa67d 100644
--- a/packages/core/src/server.ts
+++ b/packages/core/src/server.ts
@@ -85,6 +85,8 @@ export interface LowLevelStorage {
 
   rawUpdate: <T extends Doc>(domain: Domain, query: DocumentQuery<T>, operations: DocumentUpdate<T>) => Promise<void>
 
+  rawDeleteMany: <T extends Doc>(domain: Domain, query: DocumentQuery<T>) => Promise<void>
+
   // Traverse documents
   traverse: <T extends Doc>(
     domain: Domain,
diff --git a/plugins/chunter-resources/src/components/ChannelScrollView.svelte b/plugins/chunter-resources/src/components/ChannelScrollView.svelte
index a17e633496..d606e645e4 100644
--- a/plugins/chunter-resources/src/components/ChannelScrollView.svelte
+++ b/plugins/chunter-resources/src/components/ChannelScrollView.svelte
@@ -760,7 +760,7 @@
 
     if (unViewed.length === 0) {
       forceRead = true
-      const op = client.apply(undefined, 'chunter.forceReadContext')
+      const op = client.apply(undefined, 'chunter.forceReadContext', true)
       await inboxClient.readDoc(op, object._id)
       await op.commit()
     }
diff --git a/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte b/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte
index 9f8353db0b..027478ef15 100644
--- a/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte
+++ b/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte
@@ -419,7 +419,7 @@
 
     if (unViewed.length === 0) {
       forceRead = true
-      const op = client.apply(undefined, 'chunter.forceReadContext')
+      const op = client.apply(undefined, 'chunter.forceReadContext', true)
       await inboxClient.readDoc(op, object._id)
       await op.commit()
     }
diff --git a/plugins/chunter-resources/src/components/chat/utils.ts b/plugins/chunter-resources/src/components/chat/utils.ts
index 377cd196d8..203b576a06 100644
--- a/plugins/chunter-resources/src/components/chat/utils.ts
+++ b/plugins/chunter-resources/src/components/chat/utils.ts
@@ -400,7 +400,7 @@ export async function hideActivityChannels (contexts: DocNotifyContext[]): Promi
 export async function readActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
   const client = InboxNotificationsClientImpl.getClient()
   const notificationsByContext = get(client.inboxNotificationsByContext)
-  const ops = getClient().apply(undefined, 'readActivityChannels')
+  const ops = getClient().apply(undefined, 'readActivityChannels', true)
 
   try {
     for (const context of contexts) {
diff --git a/plugins/notification-resources/src/inboxNotificationsClient.ts b/plugins/notification-resources/src/inboxNotificationsClient.ts
index 19369b51d6..8d9585bfc7 100644
--- a/plugins/notification-resources/src/inboxNotificationsClient.ts
+++ b/plugins/notification-resources/src/inboxNotificationsClient.ts
@@ -235,7 +235,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
   }
 
   async archiveAllNotifications (): Promise<void> {
-    const ops = getClient().apply(undefined, 'archiveAllNotifications')
+    const ops = getClient().apply(undefined, 'archiveAllNotifications', true)
 
     try {
       const inboxNotifications = await ops.findAll(
@@ -260,7 +260,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
   }
 
   async readAllNotifications (): Promise<void> {
-    const ops = getClient().apply(undefined, 'readAllNotifications')
+    const ops = getClient().apply(undefined, 'readAllNotifications', true)
 
     try {
       const inboxNotifications = await ops.findAll(
@@ -285,7 +285,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
   }
 
   async unreadAllNotifications (): Promise<void> {
-    const ops = getClient().apply(undefined, 'unreadAllNotifications')
+    const ops = getClient().apply(undefined, 'unreadAllNotifications', true)
 
     try {
       const inboxNotifications = await ops.findAll(
diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts
index f5977d1d8d..cf07387a79 100644
--- a/plugins/notification-resources/src/utils.ts
+++ b/plugins/notification-resources/src/utils.ts
@@ -128,7 +128,7 @@ export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
   const inboxClient = InboxNotificationsClientImpl.getClient()
   const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
 
-  const ops = getClient().apply(undefined, 'readNotifyContext')
+  const ops = getClient().apply(undefined, 'readNotifyContext', true)
   try {
     await inboxClient.readNotifications(
       ops,
@@ -152,7 +152,7 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
     return
   }
 
-  const ops = getClient().apply(undefined, 'unReadNotifyContext')
+  const ops = getClient().apply(undefined, 'unReadNotifyContext', true)
 
   try {
     await inboxClient.unreadNotifications(
diff --git a/plugins/workbench-resources/src/components/WorkbenchTabPresenter.svelte b/plugins/workbench-resources/src/components/WorkbenchTabPresenter.svelte
index 12dcd0f5f0..48d6df9bdf 100644
--- a/plugins/workbench-resources/src/components/WorkbenchTabPresenter.svelte
+++ b/plugins/workbench-resources/src/components/WorkbenchTabPresenter.svelte
@@ -13,24 +13,24 @@
 // limitations under the License.
 -->
 <script lang="ts">
+  import { Asset, getResource, translate } from '@hcengineering/platform'
+  import { ComponentExtensions, getClient, reduceCalls } from '@hcengineering/presentation'
   import {
     AnySvelteComponent,
     closePanel,
     getCurrentLocation,
     Icon,
-    ModernTab,
-    navigate,
     languageStore,
-    locationToUrl
+    locationToUrl,
+    ModernTab,
+    navigate
   } from '@hcengineering/ui'
-  import { ComponentExtensions, getClient, reduceCalls } from '@hcengineering/presentation'
-  import { Asset, getResource, translate } from '@hcengineering/platform'
-  import { WorkbenchTab } from '@hcengineering/workbench'
   import view from '@hcengineering/view'
   import { showMenu } from '@hcengineering/view-resources'
+  import { WorkbenchTab } from '@hcengineering/workbench'
 
-  import { closeTab, getTabDataByLocation, getTabLocation, selectTab, tabIdStore, tabsStore } from '../workbench'
   import workbench from '../plugin'
+  import { closeTab, getTabDataByLocation, getTabLocation, selectTab, tabIdStore, tabsStore } from '../workbench'
 
   export let tab: WorkbenchTab
 
@@ -63,7 +63,9 @@
     iconProps = data.iconProps
 
     if (tab.name !== name && tab.location === locationToUrl(tabLoc)) {
+      const op = client.apply(undefined, undefined, true)
       await client.diffUpdate(tab, { name })
+      await op.commit()
     }
   }
 
diff --git a/server/core/src/mem.ts b/server/core/src/mem.ts
index 5f113d8e42..14c33d8ded 100644
--- a/server/core/src/mem.ts
+++ b/server/core/src/mem.ts
@@ -116,6 +116,8 @@ export class DummyDbAdapter implements DbAdapter {
     query: DocumentQuery<T>,
     operations: DocumentUpdate<T>
   ): Promise<void> {}
+
+  async rawDeleteMany<T extends Doc>(domain: Domain, query: DocumentQuery<T>): Promise<void> {}
 }
 
 class InMemoryAdapter extends DummyDbAdapter implements DbAdapter {
diff --git a/server/core/src/plugin.ts b/server/core/src/plugin.ts
index afd452091d..31836fef0f 100644
--- a/server/core/src/plugin.ts
+++ b/server/core/src/plugin.ts
@@ -36,8 +36,7 @@ const serverCore = plugin(serverCoreId, {
     SearchPresenter: '' as Ref<Mixin<SearchPresenter>>
   },
   space: {
-    DocIndexState: '' as Ref<Space>,
-    TriggerState: '' as Ref<Space>
+    DocIndexState: '' as Ref<Space>
   },
   metadata: {
     FrontUrl: '' as Metadata<string>,
diff --git a/server/elastic/src/backup.ts b/server/elastic/src/backup.ts
index 8ff9c2959e..72093cc57a 100644
--- a/server/elastic/src/backup.ts
+++ b/server/elastic/src/backup.ts
@@ -142,6 +142,10 @@ class ElasticDataAdapter implements DbAdapter {
     throw new Error('Method not implemented.')
   }
 
+  async rawDeleteMany<T extends Doc>(domain: Domain, query: DocumentQuery<T>): Promise<void> {
+    throw new Error('Method not implemented')
+  }
+
   async clean (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<void> {
     const indexExists = await this.client.indices.exists({
       index: this.indexName
diff --git a/server/middleware/src/lowLevel.ts b/server/middleware/src/lowLevel.ts
index a049ca089d..608aa11aeb 100644
--- a/server/middleware/src/lowLevel.ts
+++ b/server/middleware/src/lowLevel.ts
@@ -19,10 +19,10 @@ import {
   FindOptions,
   type Doc,
   type Domain,
+  type Iterator,
   type MeasureContext,
   type Ref,
-  type StorageIterator,
-  type Iterator
+  type StorageIterator
 } from '@hcengineering/core'
 import { PlatformError, unknownStatus } from '@hcengineering/platform'
 import type { Middleware, PipelineContext } from '@hcengineering/server-core'
@@ -47,36 +47,35 @@ export class LowLevelMiddleware extends BaseMiddleware implements Middleware {
         return adapterManager.getAdapter(domain, false).find(ctx, domain, recheck)
       },
 
-      async load (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
-        return await adapterManager.getAdapter(domain, false).load(ctx, domain, docs)
+      load (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
+        return adapterManager.getAdapter(domain, false).load(ctx, domain, docs)
       },
 
-      async upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise<void> {
-        await adapterManager.getAdapter(domain, true).upload(ctx, domain, docs)
+      upload (ctx: MeasureContext, domain: Domain, docs: Doc[]): Promise<void> {
+        return adapterManager.getAdapter(domain, true).upload(ctx, domain, docs)
       },
 
       async clean (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<void> {
         await adapterManager.getAdapter(domain, true).clean(ctx, domain, docs)
       },
-      async groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
-        return await adapterManager.getAdapter(domain, false).groupBy(ctx, domain, field)
+      groupBy<T>(ctx: MeasureContext, domain: Domain, field: string): Promise<Set<T>> {
+        return adapterManager.getAdapter(domain, false).groupBy(ctx, domain, field)
       },
-      async rawFindAll<T extends Doc>(domain: Domain, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<T[]> {
-        return await adapterManager.getAdapter(domain, false).rawFindAll(domain, query, options)
+      rawFindAll<T extends Doc>(domain: Domain, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<T[]> {
+        return adapterManager.getAdapter(domain, false).rawFindAll(domain, query, options)
       },
-      async rawUpdate<T extends Doc>(
-        domain: Domain,
-        query: DocumentQuery<T>,
-        operations: DocumentUpdate<T>
-      ): Promise<void> {
-        await adapterManager.getAdapter(domain, true).rawUpdate(domain, query, operations)
+      rawUpdate<T extends Doc>(domain: Domain, query: DocumentQuery<T>, operations: DocumentUpdate<T>): Promise<void> {
+        return adapterManager.getAdapter(domain, true).rawUpdate(domain, query, operations)
       },
-      async traverse<T extends Doc>(
+      rawDeleteMany (domain, query) {
+        return adapterManager.getAdapter(domain, true).rawDeleteMany(domain, query)
+      },
+      traverse<T extends Doc>(
         domain: Domain,
         query: DocumentQuery<T>,
         options?: Pick<FindOptions<T>, 'sort' | 'limit' | 'projection'>
       ): Promise<Iterator<T>> {
-        return await adapterManager.getAdapter(domain, false).traverse(domain, query, options)
+        return adapterManager.getAdapter(domain, false).traverse(domain, query, options)
       }
     }
     return undefined
diff --git a/server/mongo/src/storage.ts b/server/mongo/src/storage.ts
index 2d7d28aff4..26470c664c 100644
--- a/server/mongo/src/storage.ts
+++ b/server/mongo/src/storage.ts
@@ -262,6 +262,10 @@ abstract class MongoAdapterBase implements DbAdapter {
     }
   }
 
+  async rawDeleteMany<T extends Doc>(domain: Domain, query: DocumentQuery<T>): Promise<void> {
+    await this.db.collection(domain).deleteMany(this.translateRawQuery(query))
+  }
+
   abstract init (): Promise<void>
 
   collection<TSchema extends Document = Document>(domain: Domain): Collection<TSchema> {
diff --git a/server/postgres/src/storage.ts b/server/postgres/src/storage.ts
index 8a44e4cf50..00125c4b15 100644
--- a/server/postgres/src/storage.ts
+++ b/server/postgres/src/storage.ts
@@ -327,6 +327,13 @@ abstract class PostgresAdapterBase implements DbAdapter {
     })
   }
 
+  async rawDeleteMany<T extends Doc>(domain: Domain, query: DocumentQuery<T>): Promise<void> {
+    const translatedQuery = this.buildRawQuery(domain, query)
+    await this.retryTxn(this.client, async (client) => {
+      await client.query(`DELETE FROM ${translateDomain(domain)} WHERE ${translatedQuery}`)
+    })
+  }
+
   async findAll<T extends Doc>(
     ctx: MeasureContext<SessionData>,
     _class: Ref<Class<T>>,
diff --git a/server/server-storage/src/blobStorage.ts b/server/server-storage/src/blobStorage.ts
index a90ce52abc..209e005a7e 100644
--- a/server/server-storage/src/blobStorage.ts
+++ b/server/server-storage/src/blobStorage.ts
@@ -70,6 +70,8 @@ class StorageBlobAdapter implements DbAdapter {
     operations: DocumentUpdate<T>
   ): Promise<void> {}
 
+  async rawDeleteMany<T extends Doc>(domain: Domain, query: DocumentQuery<T>): Promise<void> {}
+
   async findAll<T extends Doc>(
     ctx: MeasureContext,
     _class: Ref<Class<T>>,
diff --git a/server/tool/src/upgrade.ts b/server/tool/src/upgrade.ts
index a6a8b9780b..0a178650c4 100644
--- a/server/tool/src/upgrade.ts
+++ b/server/tool/src/upgrade.ts
@@ -96,12 +96,6 @@ export class MigrateClientImpl implements MigrationClient {
   }
 
   async deleteMany<T extends Doc>(domain: Domain, query: DocumentQuery<T>): Promise<void> {
-    const ctx = new MeasureMetricsContext('deleteMany', {})
-    const docs = await this.lowLevel.rawFindAll(domain, query)
-    await this.lowLevel.clean(
-      ctx,
-      domain,
-      docs.map((d) => d._id)
-    )
+    await this.lowLevel.rawDeleteMany(domain, query)
   }
 }