TSK-1062: Work on Employee and EmployeeAccount migration (#2986)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-04-15 00:18:23 +07:00 committed by GitHub
parent 3ef19ed8d0
commit 62b18e1ce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 267 additions and 472 deletions

View File

@ -173,6 +173,7 @@ export class TEmployee extends TPerson implements Employee {
export class TEmployeeAccount extends TAccount implements EmployeeAccount { export class TEmployeeAccount extends TAccount implements EmployeeAccount {
employee!: Ref<Employee> employee!: Ref<Employee>
name!: string name!: string
mergedTo!: Ref<EmployeeAccount>
} }
@Model(contact.class.Organizations, core.class.Space) @Model(contact.class.Organizations, core.class.Space)

View File

@ -47,8 +47,11 @@ export function createModel (builder: Builder): void {
trigger: serverContact.trigger.OnChannelUpdate trigger: serverContact.trigger.OnChannelUpdate
}) })
builder.createDoc(serverCore.class.AsyncTrigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverContact.trigger.OnEmployeeUpdate, trigger: serverContact.trigger.OnEmployeeUpdate,
classes: [contact.class.Employee] txMatch: {
objectClass: contact.class.Employee,
_class: core.class.TxUpdateDoc
}
}) })
} }

View File

@ -14,30 +14,21 @@
// limitations under the License. // limitations under the License.
// //
import { Model, Builder } from '@hcengineering/model' import { Builder, Model } from '@hcengineering/model'
import type { Resource } from '@hcengineering/platform'
import { TClass, TDoc } from '@hcengineering/model-core' import { TClass, TDoc } from '@hcengineering/model-core'
import type { Resource } from '@hcengineering/platform'
import type {
AsyncTrigger,
ObjectDDParticipant,
Trigger,
TriggerFunc,
AsyncTriggerState,
AsyncTriggerFunc
} from '@hcengineering/server-core'
import core, { import core, {
Class, Class,
DOMAIN_MODEL,
Doc, Doc,
DocumentQuery, DocumentQuery,
DOMAIN_DOC_INDEX_STATE,
DOMAIN_MODEL,
FindOptions, FindOptions,
FindResult, FindResult,
Hierarchy, Hierarchy,
Ref, Ref
TxCUD
} from '@hcengineering/core' } from '@hcengineering/core'
import type { ObjectDDParticipant, Trigger, TriggerFunc } from '@hcengineering/server-core'
import serverCore from '@hcengineering/server-core' import serverCore from '@hcengineering/server-core'
@Model(serverCore.class.Trigger, core.class.Doc, DOMAIN_MODEL) @Model(serverCore.class.Trigger, core.class.Doc, DOMAIN_MODEL)
@ -45,18 +36,6 @@ export class TTrigger extends TDoc implements Trigger {
trigger!: Resource<TriggerFunc> trigger!: Resource<TriggerFunc>
} }
@Model(serverCore.class.AsyncTrigger, core.class.Doc, DOMAIN_MODEL)
export class TAsyncTrigger extends TDoc implements AsyncTrigger {
trigger!: Resource<AsyncTriggerFunc>
classes!: Ref<Class<Doc>>[]
}
@Model(serverCore.class.AsyncTriggerState, core.class.Doc, DOMAIN_DOC_INDEX_STATE)
export class TAsyncTriggerState extends TDoc implements AsyncTriggerState {
tx!: TxCUD<Doc>
message!: string
}
@Model(serverCore.mixin.ObjectDDParticipant, core.class.Class) @Model(serverCore.mixin.ObjectDDParticipant, core.class.Class)
export class TObjectDDParticipant extends TClass implements ObjectDDParticipant { export class TObjectDDParticipant extends TClass implements ObjectDDParticipant {
collectDocs!: Resource< collectDocs!: Resource<
@ -73,5 +52,5 @@ export class TObjectDDParticipant extends TClass implements ObjectDDParticipant
} }
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TTrigger, TObjectDDParticipant, TAsyncTriggerState, TAsyncTrigger) builder.createModel(TTrigger, TObjectDDParticipant)
} }

View File

@ -44,8 +44,11 @@ export class TOpenAIConfiguration extends TConfiguration implements OpenAIConfig
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TOpenAIConfiguration) builder.createModel(TOpenAIConfiguration)
builder.createDoc(serverCore.class.AsyncTrigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: openai.trigger.AsyncOnGPTRequest, trigger: openai.trigger.AsyncOnGPTRequest,
classes: [chunter.class.Comment, recruit.class.ApplicantMatch] txMatch: {
objectClass: { $in: [chunter.class.Comment, recruit.class.ApplicantMatch] },
_class: core.class.TxCreateDoc
}
}) })
} }

View File

@ -66,6 +66,6 @@ export interface ServerStorage extends LowLevelStorage {
options?: FindOptions<T> options?: FindOptions<T>
) => Promise<FindResult<T>> ) => Promise<FindResult<T>>
tx: (ctx: MeasureContext, tx: Tx) => Promise<[TxResult, Tx[]]> tx: (ctx: MeasureContext, tx: Tx) => Promise<[TxResult, Tx[]]>
apply: (ctx: MeasureContext, tx: Tx[], broadcast: boolean, updateTx: boolean) => Promise<Tx[]> apply: (ctx: MeasureContext, tx: Tx[], broadcast: boolean) => Promise<Tx[]>
close: () => Promise<void> close: () => Promise<void>
} }

View File

@ -17,26 +17,28 @@
import { AttachmentDocList } from '@hcengineering/attachment-resources' import { AttachmentDocList } from '@hcengineering/attachment-resources'
import type { Comment } from '@hcengineering/chunter' import type { Comment } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact' import { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { Avatar, employeeByIdStore } from '@hcengineering/contact-resources' import { Avatar, employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core' import { IdMap, Ref } from '@hcengineering/core'
import { getClient, MessageViewer } from '@hcengineering/presentation' import { MessageViewer } from '@hcengineering/presentation'
import { Icon, ShowMore, TimeSince } from '@hcengineering/ui' import { Icon, ShowMore, TimeSince } from '@hcengineering/ui'
export let value: Comment export let value: Comment
export let inline: boolean = false export let inline: boolean = false
export let disableClick = false export let disableClick = false
const client = getClient()
const cutId = (str: string): string => { const cutId = (str: string): string => {
return str.slice(0, 4) + '...' + str.slice(-4) return str.slice(0, 4) + '...' + str.slice(-4)
} }
async function getEmployee (value: Comment): Promise<Employee | undefined> { async function getEmployee (
const acc = await client.findOne(contact.class.EmployeeAccount, { _id: value.modifiedBy as Ref<EmployeeAccount> }) value: Comment,
employees: IdMap<Employee>,
accounts: IdMap<EmployeeAccount>
): Promise<Employee | undefined> {
const acc = accounts.get(value.modifiedBy as Ref<EmployeeAccount>)
if (acc !== undefined) { if (acc !== undefined) {
const emp = $employeeByIdStore.get(acc.employee) const emp = employees.get(acc.employee)
return emp return emp
} }
} }
@ -52,7 +54,7 @@
&nbsp;<span class="dark-color">#{cutId(value._id.toString())}</span> &nbsp;<span class="dark-color">#{cutId(value._id.toString())}</span>
{:else} {:else}
<div class="flex-row-top"> <div class="flex-row-top">
{#await getEmployee(value) then employee} {#await getEmployee(value, $employeeByIdStore, $employeeAccountByIdStore) then employee}
<div class="avatar"> <div class="avatar">
<Avatar size={'medium'} avatar={employee?.avatar} /> <Avatar size={'medium'} avatar={employee?.avatar} />
</div> </div>

View File

@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import chunter, { ChunterMessage } from '@hcengineering/chunter' import chunter, { ChunterMessage } from '@hcengineering/chunter'
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact' import { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources' import { Avatar, employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref, Space, toIdMap } from '@hcengineering/core' import { IdMap, Ref, Space } from '@hcengineering/core'
import { createQuery, MessageViewer } from '@hcengineering/presentation' import { MessageViewer, createQuery } from '@hcengineering/presentation'
import { Avatar } from '@hcengineering/contact-resources'
import { IconClose } from '@hcengineering/ui' import { IconClose } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { UnpinMessage } from '../index' import { UnpinMessage } from '../index'
@ -31,19 +30,14 @@
pinnedMessages = res pinnedMessages = res
}) })
const employeeAccoutsQuery = createQuery()
let employeeAcounts: IdMap<EmployeeAccount> = new Map()
employeeAccoutsQuery.query(contact.class.EmployeeAccount, {}, (res) => (employeeAcounts = toIdMap(res)))
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
function getEmployee ( function getEmployee (
message: ChunterMessage, message: ChunterMessage,
employeeAcounts: IdMap<EmployeeAccount>, employeeAccounts: IdMap<EmployeeAccount>,
employees: IdMap<Employee> employees: IdMap<Employee>
): Employee | undefined { ): Employee | undefined {
const acc = employeeAcounts.get(message.createBy as Ref<EmployeeAccount>) const acc = employeeAccounts.get(message.createBy as Ref<EmployeeAccount>)
if (acc) { if (acc) {
return employees.get(acc.employee) return employees.get(acc.employee)
} }
@ -52,7 +46,7 @@
<div class="antiPopup vScroll popup"> <div class="antiPopup vScroll popup">
{#each pinnedMessages as message} {#each pinnedMessages as message}
{@const employee = getEmployee(message, employeeAcounts, $employeeByIdStore)} {@const employee = getEmployee(message, $employeeAccountByIdStore, $employeeByIdStore)}
<div class="message"> <div class="message">
<div class="header"> <div class="header">
<div class="avatar"> <div class="avatar">

View File

@ -1,16 +1,9 @@
<script lang="ts"> <script lang="ts">
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact' import { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources' import { employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import { Account, IdMap, Ref, toIdMap } from '@hcengineering/core' import { Account, IdMap, Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
export let reactionAccounts: Ref<Account>[] export let reactionAccounts: Ref<Account>[]
let accounts: IdMap<EmployeeAccount> = new Map()
const query = createQuery()
$: query.query(contact.class.EmployeeAccount, {}, (res) => {
accounts = toIdMap(res)
})
function getAccName (acc: Ref<Account>, accounts: IdMap<EmployeeAccount>, employees: IdMap<Employee>): string { function getAccName (acc: Ref<Account>, accounts: IdMap<EmployeeAccount>, employees: IdMap<Employee>): string {
const account = accounts.get(acc as Ref<EmployeeAccount>) const account = accounts.get(acc as Ref<EmployeeAccount>)
@ -24,6 +17,6 @@
{#each reactionAccounts as acc} {#each reactionAccounts as acc}
<div> <div>
{getAccName(acc, accounts, $employeeByIdStore)} {getAccName(acc, $employeeAccountByIdStore, $employeeByIdStore)}
</div> </div>
{/each} {/each}

View File

@ -2,30 +2,26 @@
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentPreview } from '@hcengineering/attachment-resources' import { AttachmentPreview } from '@hcengineering/attachment-resources'
import { ChunterMessage } from '@hcengineering/chunter' import { ChunterMessage } from '@hcengineering/chunter'
import contact, { EmployeeAccount, getName as getContactName } from '@hcengineering/contact' import { EmployeeAccount, getName as getContactName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources' import { employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import core, { IdMap, Ref, toIdMap, WithLookup } from '@hcengineering/core' import core, { IdMap, Ref, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Label, Scroller } from '@hcengineering/ui' import { Label, Scroller } from '@hcengineering/ui'
import chunter from '../plugin' import chunter from '../plugin'
import { getTime, openMessageFromSpecial } from '../utils' import { getTime, openMessageFromSpecial } from '../utils'
import Bookmark from './icons/Bookmark.svelte'
import Message from './Message.svelte' import Message from './Message.svelte'
import Bookmark from './icons/Bookmark.svelte'
const client = getClient() const client = getClient()
let savedMessagesIds: Ref<ChunterMessage>[] = [] let savedMessagesIds: Ref<ChunterMessage>[] = []
let savedMessages: WithLookup<ChunterMessage>[] = [] let savedMessages: WithLookup<ChunterMessage>[] = []
let savedAttachmentsIds: Ref<Attachment>[] = [] let savedAttachmentsIds: Ref<Attachment>[] = []
let savedAttachments: WithLookup<Attachment>[] = [] let savedAttachments: WithLookup<Attachment>[] = []
let accounts: IdMap<EmployeeAccount> = new Map()
const messagesQuery = createQuery() const messagesQuery = createQuery()
const attachmentsQuery = createQuery() const attachmentsQuery = createQuery()
const savedMessagesQuery = createQuery() const savedMessagesQuery = createQuery()
const savedAttachmentsQuery = createQuery() const savedAttachmentsQuery = createQuery()
const accQ = createQuery()
accQ.query(contact.class.EmployeeAccount, {}, (res) => (accounts = toIdMap(res)))
savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => { savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
savedMessagesIds = res.map((r) => r.attachedTo) savedMessagesIds = res.map((r) => r.attachedTo)
@ -78,8 +74,8 @@
}) })
} }
function getName (a: Attachment): string | undefined { function getName (a: Attachment, employeeAccountByIdStore: IdMap<EmployeeAccount>): string | undefined {
const acc = accounts.get(a.modifiedBy as Ref<EmployeeAccount>) const acc = employeeAccountByIdStore.get(a.modifiedBy as Ref<EmployeeAccount>)
if (acc !== undefined) { if (acc !== undefined) {
const emp = $employeeByIdStore.get(acc?.employee) const emp = $employeeByIdStore.get(acc?.employee)
if (emp !== undefined) { if (emp !== undefined) {
@ -112,7 +108,10 @@
<div class="attachmentContainer" on:click={() => openAttachment(att)}> <div class="attachmentContainer" on:click={() => openAttachment(att)}>
<AttachmentPreview value={att} isSaved={true} /> <AttachmentPreview value={att} isSaved={true} />
<div class="label"> <div class="label">
<Label label={chunter.string.SharedBy} params={{ name: getName(att), time: getTime(att.modifiedOn) }} /> <Label
label={chunter.string.SharedBy}
params={{ name: getName(att, $employeeAccountByIdStore), time: getTime(att.modifiedOn) }}
/>
</div> </div>
</div> </div>
{/each} {/each}

View File

@ -18,6 +18,7 @@
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { ButtonKind, ButtonSize } from '@hcengineering/ui' import { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { employeeAccountByIdStore } from '../utils'
import UserBoxList from './UserBoxList.svelte' import UserBoxList from './UserBoxList.svelte'
export let label: IntlString export let label: IntlString
@ -40,14 +41,6 @@
}, 500) }, 500)
} }
const accountQuery = createQuery()
let accounts: Account[] = []
$: accountQuery.query(core.class.Account, { _id: { $in: value } }, (res) => {
accounts = res
})
const excludedQuery = createQuery() const excludedQuery = createQuery()
let excluded: Account[] = [] let excluded: Account[] = []
@ -61,7 +54,9 @@
excluded = [] excluded = []
} }
$: employess = accounts.map((it) => (it as EmployeeAccount).employee) $: employees = Array.from(
(value ?? []).map((it) => $employeeAccountByIdStore.get(it as Ref<EmployeeAccount>)?.employee)
).filter((it) => it !== undefined) as Ref<Employee>[]
$: docQuery = $: docQuery =
excluded.length > 0 excluded.length > 0
@ -75,7 +70,7 @@
</script> </script>
<UserBoxList <UserBoxList
items={employess} items={employees}
{label} {label}
{readonly} {readonly}
{docQuery} {docQuery}

View File

@ -13,13 +13,13 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Employee, EmployeeAccount } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import { Account, DocumentQuery, Ref } from '@hcengineering/core' import { Account, DocumentQuery, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { ButtonKind, ButtonSize } from '@hcengineering/ui' import { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import contact from '../plugin' import contact from '../plugin'
import { employeeAccountByIdStore } from '../utils'
import UserBox from './UserBox.svelte' import UserBox from './UserBox.svelte'
export let label: IntlString = contact.string.Employee export let label: IntlString = contact.string.Employee
@ -29,16 +29,10 @@
export let size: ButtonSize = 'small' export let size: ButtonSize = 'small'
export let readonly = false export let readonly = false
const query = createQuery() $: accounts = Array.from($employeeAccountByIdStore.values())
let accounts: EmployeeAccount[] = []
query.query<EmployeeAccount>(contact.class.EmployeeAccount, docQuery as DocumentQuery<EmployeeAccount>, (res) => {
accounts = res
map = new Map(res.map((p) => [p.employee, p._id]))
})
let map: Map<Ref<Employee>, Ref<Account>> = new Map() let map: Map<Ref<Employee>, Ref<Account>> = new Map()
$: map = new Map(accounts.map((p) => [p.employee, p._id]))
$: employees = accounts.map((p) => p.employee) $: employees = accounts.map((p) => p.employee)
$: selectedEmp = value && accounts.find((p) => p._id === value)?.employee $: selectedEmp = value && accounts.find((p) => p._id === value)?.employee

View File

@ -16,18 +16,13 @@
<script lang="ts"> <script lang="ts">
import { EmployeeAccount } from '@hcengineering/contact' import { EmployeeAccount } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core' import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { employeeAccountByIdStore } from '../utils'
import contact from '../plugin'
import EmployeeAccountPresenter from './EmployeeAccountPresenter.svelte' import EmployeeAccountPresenter from './EmployeeAccountPresenter.svelte'
export let value: Ref<EmployeeAccount> export let value: Ref<EmployeeAccount>
export let disabled = false export let disabled = false
let account: EmployeeAccount | undefined $: account = $employeeAccountByIdStore.get(value)
const query = createQuery()
$: query.query(contact.class.EmployeeAccount, { _id: value }, (r) => ([account] = r))
</script> </script>
{#if account} {#if account}

View File

@ -39,7 +39,12 @@
const query: DocumentQuery<EmployeeAccount> = const query: DocumentQuery<EmployeeAccount> =
isSearch > 0 ? { name: { $like: '%' + search + '%' } } : { _id: { $in: accounts as Ref<EmployeeAccount>[] } } isSearch > 0 ? { name: { $like: '%' + search + '%' } } : { _id: { $in: accounts as Ref<EmployeeAccount>[] } }
const employess = await client.findAll(contact.class.EmployeeAccount, query) const employess = await client.findAll(contact.class.EmployeeAccount, query)
members = new Set(employess.filter((p) => accounts.includes(p._id)).map((p) => p.employee)) members = new Set(
employess
.filter((it) => it.mergedTo == null)
.filter((p) => accounts.includes(p._id))
.map((p) => p.employee)
)
return await client.findAll( return await client.findAll(
contact.class.Employee, contact.class.Employee,
{ {

View File

@ -223,6 +223,9 @@ async function generateLocation (loc: Location, id: Ref<Contact>): Promise<Resol
export const employeeByIdStore = writable<IdMap<Employee>>(new Map()) export const employeeByIdStore = writable<IdMap<Employee>>(new Map())
export const employeesStore = writable<Employee[]>([]) export const employeesStore = writable<Employee[]>([])
export const employeeAccountByIdStore = writable<IdMap<EmployeeAccount>>(new Map())
export const channelProviders = writable<ChannelProvider[]>([]) export const channelProviders = writable<ChannelProvider[]>([])
function fillStores (): void { function fillStores (): void {
@ -234,6 +237,22 @@ function fillStores (): void {
employeeByIdStore.set(toIdMap(res)) employeeByIdStore.set(toIdMap(res))
}) })
const accountQ = createQuery(true)
accountQ.query(contact.class.EmployeeAccount, {}, (res) => {
const mergedEmployees = res.filter((it) => it.mergedTo !== undefined)
const activeEmployees = res.filter((it) => it.mergedTo === undefined)
const ids = toIdMap(activeEmployees)
for (const e of mergedEmployees) {
if (e.mergedTo !== undefined) {
const mergeTo = ids.get(e.mergedTo)
if (mergeTo !== undefined) {
ids.set(e._id, mergeTo)
}
}
}
employeeAccountByIdStore.set(ids)
})
const providerQuery = createQuery(true) const providerQuery = createQuery(true)
providerQuery.query(contact.class.ChannelProvider, {}, (res) => channelProviders.set(res)) providerQuery.query(contact.class.ChannelProvider, {}, (res) => channelProviders.set(res))
} else { } else {

View File

@ -151,6 +151,7 @@ export interface Employee extends Person {
export interface EmployeeAccount extends Account { export interface EmployeeAccount extends Account {
employee: Ref<Employee> employee: Ref<Employee>
name: string name: string
mergedTo?: Ref<EmployeeAccount>
} }
/** /**

View File

@ -14,9 +14,9 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Channel, Contact, EmployeeAccount } from '@hcengineering/contact' import { Channel, Contact } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources' import { employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref, SortingOrder, toIdMap } from '@hcengineering/core' import { Ref, SortingOrder } from '@hcengineering/core'
import { Message, SharedMessage } from '@hcengineering/gmail' import { Message, SharedMessage } from '@hcengineering/gmail'
import { NotificationClientImpl } from '@hcengineering/notification-resources' import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
@ -32,14 +32,11 @@
export let enabled: boolean export let enabled: boolean
let messages: Message[] = [] let messages: Message[] = []
let accounts: IdMap<EmployeeAccount> = new Map()
let selected: Set<Ref<SharedMessage>> = new Set<Ref<SharedMessage>>() let selected: Set<Ref<SharedMessage>> = new Set<Ref<SharedMessage>>()
let selectable = false let selectable = false
const messagesQuery = createQuery() const messagesQuery = createQuery()
const accountsQuery = createQuery()
accountsQuery.query(contact.class.EmployeeAccount, {}, (res) => (accounts = toIdMap(res)))
const notificationClient = NotificationClientImpl.getClient() const notificationClient = NotificationClientImpl.getClient()
@ -68,7 +65,7 @@
object._class, object._class,
'gmailSharedMessages', 'gmailSharedMessages',
{ {
messages: convertMessages(object, channel, selectedMessages, accounts, $employeeByIdStore) messages: convertMessages(object, channel, selectedMessages, $employeeAccountByIdStore, $employeeByIdStore)
} }
) )
await notificationClient.updateLastView(channel._id, channel._class, undefined, true) await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
@ -126,7 +123,7 @@
<div class="popupPanel-body__main-content py-4 clear-mins flex-no-shrink"> <div class="popupPanel-body__main-content py-4 clear-mins flex-no-shrink">
{#if messages && messages.length > 0} {#if messages && messages.length > 0}
<Messages <Messages
messages={convertMessages(object, channel, messages, accounts, $employeeByIdStore)} messages={convertMessages(object, channel, messages, $employeeAccountByIdStore, $employeeByIdStore)}
{selectable} {selectable}
bind:selected bind:selected
on:select on:select

View File

@ -15,8 +15,8 @@
<script lang="ts"> <script lang="ts">
import { TxViewlet } from '@hcengineering/activity' import { TxViewlet } from '@hcengineering/activity'
import { ActivityKey } from '@hcengineering/activity-resources' import { ActivityKey } from '@hcengineering/activity-resources'
import contact, { EmployeeAccount, getName } from '@hcengineering/contact' import { EmployeeAccount, getName } from '@hcengineering/contact'
import { Avatar, employeeByIdStore } from '@hcengineering/contact-resources' import { Avatar, employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import core, { Doc, Ref, TxCUD, TxProcessor } from '@hcengineering/core' import core, { Doc, Ref, TxCUD, TxProcessor } from '@hcengineering/core'
import notification, { DocUpdates } from '@hcengineering/notification' import notification, { DocUpdates } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
@ -54,9 +54,8 @@
getResource(presenterRes).then((res) => (presenter = res)) getResource(presenterRes).then((res) => (presenter = res))
} }
const query = createQuery() $: account = $employeeAccountByIdStore.get(tx?.modifiedBy as Ref<EmployeeAccount>)
$: tx &&
query.query(contact.class.EmployeeAccount, { _id: tx.modifiedBy as Ref<EmployeeAccount> }, (r) => ([account] = r))
$: employee = account && $employeeByIdStore.get(account.employee) $: employee = account && $employeeByIdStore.get(account.employee)
const docQuery = createQuery() const docQuery = createQuery()

View File

@ -16,15 +16,16 @@
import { DisplayTx, TxViewlet } from '@hcengineering/activity' import { DisplayTx, TxViewlet } from '@hcengineering/activity'
import { import {
ActivityKey, ActivityKey,
TxDisplayViewlet,
getValue, getValue,
newDisplayTx, newDisplayTx,
TxDisplayViewlet,
updateViewlet updateViewlet
} from '@hcengineering/activity-resources' } from '@hcengineering/activity-resources'
import activity from '@hcengineering/activity-resources/src/plugin' import activity from '@hcengineering/activity-resources/src/plugin'
import contact, { EmployeeAccount } from '@hcengineering/contact' import { EmployeeAccount } from '@hcengineering/contact'
import { employeeAccountByIdStore } from '@hcengineering/contact-resources'
import core, { AnyAttribute, Doc, Ref, TxCUD } from '@hcengineering/core' import core, { AnyAttribute, Doc, Ref, TxCUD } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Component, Label, ShowMore } from '@hcengineering/ui' import { Component, Label, ShowMore } from '@hcengineering/ui'
import type { AttributeModel } from '@hcengineering/view' import type { AttributeModel } from '@hcengineering/view'
import { ObjectPresenter } from '@hcengineering/view-resources' import { ObjectPresenter } from '@hcengineering/view-resources'
@ -52,8 +53,6 @@
model = [] model = []
} }
const query = createQuery()
function getProps (props: any): any { function getProps (props: any): any {
return { ...props, attr: ptx?.collectionAttribute } return { ...props, attr: ptx?.collectionAttribute }
} }
@ -67,14 +66,7 @@
} }
}) })
$: query.query( $: employee = $employeeAccountByIdStore.get(tx.modifiedBy as Ref<EmployeeAccount>)
contact.class.EmployeeAccount,
{ _id: tx.modifiedBy as Ref<EmployeeAccount> },
(account) => {
;[employee] = account
},
{ limit: 1 }
)
function isMessageType (attr?: AnyAttribute): boolean { function isMessageType (attr?: AnyAttribute): boolean {
return attr?.type._class === core.class.TypeMarkup return attr?.type._class === core.class.TypeMarkup

View File

@ -16,10 +16,11 @@
import { DisplayTx, TxViewlet } from '@hcengineering/activity' import { DisplayTx, TxViewlet } from '@hcengineering/activity'
import { ActivityKey, getValue, newDisplayTx, updateViewlet } from '@hcengineering/activity-resources' import { ActivityKey, getValue, newDisplayTx, updateViewlet } from '@hcengineering/activity-resources'
import activity from '@hcengineering/activity-resources/src/plugin' import activity from '@hcengineering/activity-resources/src/plugin'
import contact, { EmployeeAccount } from '@hcengineering/contact' import { EmployeeAccount } from '@hcengineering/contact'
import { employeeAccountByIdStore } from '@hcengineering/contact-resources'
import core, { AnyAttribute, Doc, Ref, Tx, TxCUD } from '@hcengineering/core' import core, { AnyAttribute, Doc, Ref, Tx, TxCUD } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui' import { Label } from '@hcengineering/ui'
import type { AttributeModel } from '@hcengineering/view' import type { AttributeModel } from '@hcengineering/view'
import { ObjectPresenter } from '@hcengineering/view-resources' import { ObjectPresenter } from '@hcengineering/view-resources'
@ -37,7 +38,7 @@
let modelIcon: Asset | undefined = undefined let modelIcon: Asset | undefined = undefined
$: if (tx._id !== ptx?.tx._id) { $: if (tx._id !== ptx?.tx._id) {
ptx = newDisplayTx(tx as TxCUD<Doc>, client.getHierarchy()) ptx = newDisplayTx(tx as TxCUD<Doc>, client.getHierarchy(), false)
if (tx.modifiedBy !== employee?._id) { if (tx.modifiedBy !== employee?._id) {
employee = undefined employee = undefined
} }
@ -45,8 +46,6 @@
model = [] model = []
} }
const query = createQuery()
$: ptx && $: ptx &&
updateViewlet(client, viewlets, ptx).then((result) => { updateViewlet(client, viewlets, ptx).then((result) => {
if (result.id === tx._id) { if (result.id === tx._id) {
@ -56,14 +55,7 @@
} }
}) })
$: query.query( $: employee = $employeeAccountByIdStore.get(tx.modifiedBy as Ref<EmployeeAccount>)
contact.class.EmployeeAccount,
{ _id: tx.modifiedBy as Ref<EmployeeAccount> },
(account) => {
;[employee] = account
},
{ limit: 1 }
)
function isMessageType (attr?: AnyAttribute): boolean { function isMessageType (attr?: AnyAttribute): boolean {
return attr?.type._class === core.class.TypeMarkup return attr?.type._class === core.class.TypeMarkup

View File

@ -23,7 +23,8 @@
EmployeeAccount, EmployeeAccount,
getName as getContactName getName as getContactName
} from '@hcengineering/contact' } from '@hcengineering/contact'
import { Class, generateId, getCurrentAccount, IdMap, Ref, SortingOrder } from '@hcengineering/core' import { Avatar, employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import { Class, IdMap, Ref, SortingOrder, generateId, getCurrentAccount } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources' import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getEmbeddedLabel, getResource } from '@hcengineering/platform' import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
@ -32,22 +33,21 @@
import templates, { TemplateDataProvider } from '@hcengineering/templates' import templates, { TemplateDataProvider } from '@hcengineering/templates'
import { import {
Button, Button,
eventToHTMLElement,
Icon, Icon,
IconShare, IconShare,
Label, Label,
Panel, Panel,
Scroller, Scroller,
eventToHTMLElement,
showPopup, showPopup,
tooltip tooltip
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { Avatar, employeeByIdStore } from '@hcengineering/contact-resources'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import telegram from '../plugin' import telegram from '../plugin'
import Connect from './Connect.svelte' import Connect from './Connect.svelte'
import TelegramIcon from './icons/Telegram.svelte'
import Messages from './Messages.svelte' import Messages from './Messages.svelte'
import Reconnect from './Reconnect.svelte' import Reconnect from './Reconnect.svelte'
import TelegramIcon from './icons/Telegram.svelte'
export let _id: Ref<Contact> export let _id: Ref<Contact>
export let _class: Ref<Class<Contact>> export let _class: Ref<Class<Contact>>
@ -95,13 +95,11 @@
}) })
let messages: TelegramMessage[] = [] let messages: TelegramMessage[] = []
let accounts: EmployeeAccount[] = []
let integration: Integration | undefined let integration: Integration | undefined
let selected: Set<Ref<SharedTelegramMessage>> = new Set<Ref<SharedTelegramMessage>>() let selected: Set<Ref<SharedTelegramMessage>> = new Set<Ref<SharedTelegramMessage>>()
let selectable = false let selectable = false
const messagesQuery = createQuery() const messagesQuery = createQuery()
const accauntsQuery = createQuery()
const settingsQuery = createQuery() const settingsQuery = createQuery()
const accountId = getCurrentAccount()._id const accountId = getCurrentAccount()._id
@ -127,10 +125,6 @@
$: channel && updateMessagesQuery(channel._id) $: channel && updateMessagesQuery(channel._id)
accauntsQuery.query(contact.class.EmployeeAccount, {}, (result) => {
accounts = result
})
settingsQuery.query( settingsQuery.query(
setting.class.Integration, setting.class.Integration,
{ type: telegram.integrationType.Telegram, createdBy: accountId }, { type: telegram.integrationType.Telegram, createdBy: accountId },
@ -160,8 +154,8 @@
loading = false loading = false
} }
function getName (message: TelegramMessage, accounts: EmployeeAccount[]): string { function getName (message: TelegramMessage, accounts: IdMap<EmployeeAccount>): string {
return message.incoming ? object.name : accounts.find((p) => p._id === message.modifiedBy)?.name ?? '' return message.incoming ? object.name : accounts.get(message.modifiedBy as Ref<EmployeeAccount>)?.name ?? ''
} }
async function share (): Promise<void> { async function share (): Promise<void> {
@ -173,7 +167,7 @@
object._class, object._class,
'sharedTelegramMessages', 'sharedTelegramMessages',
{ {
messages: convertMessages(selectedMessages, accounts) messages: convertMessages(selectedMessages, $employeeAccountByIdStore)
} }
) )
if (channel !== undefined) { if (channel !== undefined) {
@ -188,7 +182,7 @@
selected = selected selected = selected
} }
function convertMessages (messages: TelegramMessage[], accounts: EmployeeAccount[]): SharedTelegramMessage[] { function convertMessages (messages: TelegramMessage[], accounts: IdMap<EmployeeAccount>): SharedTelegramMessage[] {
return messages.map((m) => { return messages.map((m) => {
return { return {
...m, ...m,
@ -219,16 +213,16 @@
function getParticipants ( function getParticipants (
messages: TelegramMessage[], messages: TelegramMessage[],
accounts: EmployeeAccount[], accounts: IdMap<EmployeeAccount>,
object: Contact | undefined, object: Contact | undefined,
employees: IdMap<Employee> employees: IdMap<Employee>
): Contact[] { ): Contact[] {
if (object === undefined || accounts.length === 0) return [] if (object === undefined || accounts.size === 0) return []
const res: IdMap<Contact> = new Map() const res: IdMap<Contact> = new Map()
res.set(object._id, object) res.set(object._id, object)
const accs = new Set(messages.map((p) => p.modifiedBy)) const accs = new Set(messages.map((p) => p.modifiedBy))
for (const acc of accs) { for (const acc of accs) {
const account = accounts.find((p) => p._id === acc) const account = accounts.get(acc as Ref<EmployeeAccount>)
if (account !== undefined) { if (account !== undefined) {
const emp = employees.get(account.employee) const emp = employees.get(account.employee)
if (emp !== undefined) { if (emp !== undefined) {
@ -239,7 +233,7 @@
return Array.from(res.values()) return Array.from(res.values())
} }
$: participants = getParticipants(messages, accounts, object, $employeeByIdStore) $: participants = getParticipants(messages, $employeeAccountByIdStore, object, $employeeByIdStore)
</script> </script>
{#if object !== undefined} {#if object !== undefined}
@ -298,8 +292,8 @@
</svelte:fragment> </svelte:fragment>
<Scroller bottomStart autoscroll> <Scroller bottomStart autoscroll>
{#if messages && accounts} {#if messages}
<Messages messages={convertMessages(messages, accounts)} {selectable} bind:selected /> <Messages messages={convertMessages(messages, $employeeAccountByIdStore)} {selectable} bind:selected />
{/if} {/if}
</Scroller> </Scroller>

View File

@ -13,15 +13,15 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { EmployeeAccount } from '@hcengineering/contact' import { EmployeeAccount } from '@hcengineering/contact'
import { employeeByIdStore, EmployeeBox } from '@hcengineering/contact-resources' import { EmployeeBox, employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import core, { ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core' import core, { ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core'
import { AttributeBarEditor, createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation' import { AttributeBarEditor, KeyedAttribute, createQuery, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags' import tags from '@hcengineering/tags'
import type { Issue } from '@hcengineering/tracker' import type { Issue } from '@hcengineering/tracker'
import { Component, Label } from '@hcengineering/ui' import { Component, Label } from '@hcengineering/ui'
import { getFiltredKeys, isCollectionAttr, ObjectBox } from '@hcengineering/view-resources' import { ObjectBox, getFiltredKeys, isCollectionAttr } from '@hcengineering/view-resources'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import ComponentEditor from '../../components/ComponentEditor.svelte' import ComponentEditor from '../../components/ComponentEditor.svelte'
import SprintEditor from '../../sprints/SprintEditor.svelte' import SprintEditor from '../../sprints/SprintEditor.svelte'
@ -87,19 +87,10 @@
'blockedBy' 'blockedBy'
]) ])
const employeeAccountQuery = createQuery()
let account: EmployeeAccount | undefined let account: EmployeeAccount | undefined
$: employee = account && $employeeByIdStore.get(account.employee) $: employee = account && $employeeByIdStore.get(account.employee)
$: employeeAccountQuery.query( $: account = $employeeAccountByIdStore.get(issue.createdBy as Ref<EmployeeAccount>)
contact.class.EmployeeAccount,
{ _id: issue.createdBy as Ref<EmployeeAccount> },
(res) => {
;[account] = res
},
{ limit: 1 }
)
</script> </script>
<div class="content"> <div class="content">

View File

@ -66,7 +66,7 @@ export async function connect (title: string): Promise<Client | undefined> {
void (async () => { void (async () => {
const newVersion = await _client?.findOne<Version>(core.class.Version, {}) const newVersion = await _client?.findOne<Version>(core.class.Version, {})
console.log('Reconnect Model version', version) console.log('Reconnect Model version', newVersion)
const currentVersionStr = versionToString(version as Version) const currentVersionStr = versionToString(version as Version)
const reconnectVersionStr = versionToString(newVersion as Version) const reconnectVersionStr = versionToString(newVersion as Version)

View File

@ -41,7 +41,7 @@ import core, {
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { Collaborators } from '@hcengineering/notification' import notification, { Collaborators } from '@hcengineering/notification'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import serverCore, { AsyncTriggerControl, TriggerControl } from '@hcengineering/server-core' import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
/** /**
@ -110,13 +110,13 @@ export async function OnContactDelete (
} }
async function updateAllRefs ( async function updateAllRefs (
control: AsyncTriggerControl, control: TriggerControl,
sourceAccount: EmployeeAccount, sourceAccount: EmployeeAccount,
targetAccount: EmployeeAccount, targetAccount: EmployeeAccount,
modifiedOn: Timestamp, modifiedOn: Timestamp,
modifiedBy: Ref<Account> modifiedBy: Ref<Account>
): Promise<Tx[]> { ): Promise<Tx[]> {
const res: Tx[] = [] console.log('merge employee:', sourceAccount.name, 'to', targetAccount.name)
// Move all possible references to Account and Employee and replace to target one. // Move all possible references to Account and Employee and replace to target one.
const reftos = (await control.modelDb.findAll(core.class.Attribute, { 'type._class': core.class.RefTo })).filter( const reftos = (await control.modelDb.findAll(core.class.Attribute, { 'type._class': core.class.RefTo })).filter(
(it) => { (it) => {
@ -133,6 +133,9 @@ async function updateAllRefs (
if (to.to === contact.class.Employee) { if (to.to === contact.class.Employee) {
const descendants = control.hierarchy.getDescendants(attr.attributeOf) const descendants = control.hierarchy.getDescendants(attr.attributeOf)
for (const d of descendants) { for (const d of descendants) {
if (control.hierarchy.isDerived(d, core.class.Tx)) {
continue
}
if (control.hierarchy.findDomain(d) !== undefined) { if (control.hierarchy.findDomain(d) !== undefined) {
while (true) { while (true) {
const values = await control.findAll(d, { [attr.name]: sourceAccount.employee }, { limit: 100 }) const values = await control.findAll(d, { [attr.name]: sourceAccount.employee }, { limit: 100 })
@ -144,7 +147,10 @@ async function updateAllRefs (
for (const v of values) { for (const v of values) {
await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount.employee, targetAccount._id) await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount.employee, targetAccount._id)
} }
await control.apply(builder.txes, true, true) if (builder.txes.length > 0) {
console.log('merge employee:', sourceAccount.name, 'to', targetAccount.name, d, builder.txes.length)
await control.apply(builder.txes, false)
}
} }
} }
} }
@ -156,6 +162,9 @@ async function updateAllRefs (
) { ) {
const descendants = control.hierarchy.getDescendants(attr.attributeOf) const descendants = control.hierarchy.getDescendants(attr.attributeOf)
for (const d of descendants) { for (const d of descendants) {
if (control.hierarchy.isDerived(d, core.class.Tx)) {
continue
}
if (control.hierarchy.findDomain(d) !== undefined) { if (control.hierarchy.findDomain(d) !== undefined) {
while (true) { while (true) {
const values = await control.findAll(d, { [attr.name]: sourceAccount._id }, { limit: 100 }) const values = await control.findAll(d, { [attr.name]: sourceAccount._id }, { limit: 100 })
@ -166,7 +175,10 @@ async function updateAllRefs (
for (const v of values) { for (const v of values) {
await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount._id, targetAccount._id) await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount._id, targetAccount._id)
} }
await control.apply(builder.txes, true, true) if (builder.txes.length > 0) {
console.log('merge employee:', sourceAccount.name, 'to', targetAccount.name, d, builder.txes.length)
await control.apply(builder.txes, false)
}
} }
} }
} }
@ -181,6 +193,9 @@ async function updateAllRefs (
if (to.to === contact.class.Employee) { if (to.to === contact.class.Employee) {
const descendants = control.hierarchy.getDescendants(attr.attributeOf) const descendants = control.hierarchy.getDescendants(attr.attributeOf)
for (const d of descendants) { for (const d of descendants) {
if (control.hierarchy.isDerived(d, core.class.Tx)) {
continue
}
if (control.hierarchy.findDomain(d) !== undefined) { if (control.hierarchy.findDomain(d) !== undefined) {
while (true) { while (true) {
const values = await control.findAll( const values = await control.findAll(
@ -195,7 +210,10 @@ async function updateAllRefs (
for (const v of values) { for (const v of values) {
await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount.employee, targetAccount._id) await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount.employee, targetAccount._id)
} }
await control.apply(builder.txes, true, true) if (builder.txes.length > 0) {
console.log('merge employee:', sourceAccount.name, 'to', targetAccount.name, d, builder.txes.length)
await control.apply(builder.txes, false)
}
} }
} }
} }
@ -207,6 +225,9 @@ async function updateAllRefs (
) { ) {
const descendants = control.hierarchy.getDescendants(attr.attributeOf) const descendants = control.hierarchy.getDescendants(attr.attributeOf)
for (const d of descendants) { for (const d of descendants) {
if (control.hierarchy.isDerived(d, core.class.Tx)) {
continue
}
if (control.hierarchy.findDomain(d) !== undefined) { if (control.hierarchy.findDomain(d) !== undefined) {
while (true) { while (true) {
const values = await control.findAll(d, { [attr.name]: sourceAccount._id }, { limit: 100 }) const values = await control.findAll(d, { [attr.name]: sourceAccount._id }, { limit: 100 })
@ -217,27 +238,30 @@ async function updateAllRefs (
for (const v of values) { for (const v of values) {
await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount._id, targetAccount._id) await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount._id, targetAccount._id)
} }
await control.apply(builder.txes, true, true) if (builder.txes.length > 0) {
console.log('merge employee:', sourceAccount.name, 'to', targetAccount.name, d, builder.txes.length)
await control.apply(builder.txes, false)
}
} }
} }
} }
} }
} }
const employee = (await control.findAll(contact.class.Employee, { _id: sourceAccount.employee })).shift() const employee = (await control.findAll(contact.class.Employee, { _id: sourceAccount.employee })).shift()
const builder = new TxBuilder(control.hierarchy, control.modelDb, modifiedBy) const builder = new TxBuilder(control.hierarchy, control.modelDb, modifiedBy)
await builder.remove(sourceAccount)
if (employee !== undefined) { if (employee !== undefined) {
await builder.remove(employee) await builder.remove(employee)
} }
await control.apply(builder.txes, true, true) await builder.update(sourceAccount, { mergedTo: targetAccount._id })
await control.apply(builder.txes, true)
return res return []
} }
async function mergeEmployee (control: AsyncTriggerControl, uTx: TxUpdateDoc<Employee>): Promise<Tx[]> { async function mergeEmployee (control: TriggerControl, uTx: TxUpdateDoc<Employee>): Promise<Tx[]> {
if (uTx.operations.mergedTo === undefined) return [] if (uTx.operations.mergedTo === undefined) return []
const target = uTx.operations.mergedTo const target = uTx.operations.mergedTo
const res: Tx[] = []
const attributes = control.hierarchy.getAllAttributes(contact.class.Employee) const attributes = control.hierarchy.getAllAttributes(contact.class.Employee)
@ -245,7 +269,12 @@ async function mergeEmployee (control: AsyncTriggerControl, uTx: TxUpdateDoc<Emp
if (control.hierarchy.isDerived(attribute[1].type._class, core.class.Collection)) { if (control.hierarchy.isDerived(attribute[1].type._class, core.class.Collection)) {
if (attribute[1]._id === contact.class.Contact + '_channels') continue if (attribute[1]._id === contact.class.Contact + '_channels') continue
const collection = attribute[1].type as Collection<AttachedDoc> const collection = attribute[1].type as Collection<AttachedDoc>
const allAttached = await control.findAll(collection.of, { attachedTo: uTx.objectId }) const res: Tx[] = []
while (true) {
const allAttached = await control.findAll(collection.of, { attachedTo: uTx.objectId }, { limit: 100 })
if (allAttached.length === 0) {
break
}
for (const attached of allAttached) { for (const attached of allAttached) {
const tx = control.txFactory.createTxUpdateDoc(attached._class, attached.space, attached._id, { const tx = control.txFactory.createTxUpdateDoc(attached._class, attached.space, attached._id, {
attachedTo: target attachedTo: target
@ -259,6 +288,8 @@ async function mergeEmployee (control: AsyncTriggerControl, uTx: TxUpdateDoc<Emp
) )
res.push(parent) res.push(parent)
} }
await control.apply(res, false)
}
} }
} }
@ -267,32 +298,18 @@ async function mergeEmployee (control: AsyncTriggerControl, uTx: TxUpdateDoc<Emp
)[0] )[0]
const newEmployeeAccount = (await control.modelDb.findAll(contact.class.EmployeeAccount, { employee: target }))[0] const newEmployeeAccount = (await control.modelDb.findAll(contact.class.EmployeeAccount, { employee: target }))[0]
if (oldEmployeeAccount === undefined || newEmployeeAccount === undefined) return res if (oldEmployeeAccount === undefined || newEmployeeAccount === undefined) {
const accountTxes = await updateAllRefs( return []
control, }
oldEmployeeAccount, return await updateAllRefs(control, oldEmployeeAccount, newEmployeeAccount, uTx.modifiedOn, uTx.modifiedBy)
newEmployeeAccount,
uTx.modifiedOn,
uTx.modifiedBy
)
res.push(...accountTxes)
return res
} }
/** /**
* @public * @public
*/ */
export async function OnEmployeeUpdate (tx: Tx, control: AsyncTriggerControl): Promise<Tx[]> { export async function OnEmployeeUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
if (tx._class !== core.class.TxUpdateDoc) {
return []
}
const uTx = tx as TxUpdateDoc<Employee> const uTx = tx as TxUpdateDoc<Employee>
if (!control.hierarchy.isDerived(uTx.objectClass, contact.class.Employee)) {
return []
}
const result: Tx[] = [] const result: Tx[] = []
const txes = await mergeEmployee(control, uTx) const txes = await mergeEmployee(control, uTx)

View File

@ -14,9 +14,9 @@
// limitations under the License. // limitations under the License.
// //
import type { Resource, Plugin } from '@hcengineering/platform' import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import type { AsyncTriggerFunc, TriggerFunc } from '@hcengineering/server-core' import type { TriggerFunc } from '@hcengineering/server-core'
import { Presenter } from '@hcengineering/server-notification' import { Presenter } from '@hcengineering/server-notification'
/** /**
@ -31,7 +31,7 @@ export default plugin(serverContactId, {
trigger: { trigger: {
OnContactDelete: '' as Resource<TriggerFunc>, OnContactDelete: '' as Resource<TriggerFunc>,
OnChannelUpdate: '' as Resource<TriggerFunc>, OnChannelUpdate: '' as Resource<TriggerFunc>,
OnEmployeeUpdate: '' as Resource<AsyncTriggerFunc> OnEmployeeUpdate: '' as Resource<TriggerFunc>
}, },
function: { function: {
PersonHTMLPresenter: '' as Resource<Presenter>, PersonHTMLPresenter: '' as Resource<Presenter>,

View File

@ -17,7 +17,7 @@ import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import type { Account, Class, Ref } from '@hcengineering/core' import type { Account, Class, Ref } from '@hcengineering/core'
import { AsyncTriggerFunc } from '@hcengineering/server-core' import { TriggerFunc } from '@hcengineering/server-core'
import type { OpenAIConfiguration } from './types' import type { OpenAIConfiguration } from './types'
export * from './types' export * from './types'
@ -31,7 +31,7 @@ export const openAIId = 'openai' as Plugin
*/ */
const openaiPlugin = plugin(openAIId, { const openaiPlugin = plugin(openAIId, {
trigger: { trigger: {
AsyncOnGPTRequest: '' as Resource<AsyncTriggerFunc> AsyncOnGPTRequest: '' as Resource<TriggerFunc>
}, },
class: { class: {
OpenAIConfiguration: '' as Ref<Class<OpenAIConfiguration>> OpenAIConfiguration: '' as Ref<Class<OpenAIConfiguration>>

View File

@ -27,7 +27,7 @@ import core, {
TxProcessor TxProcessor
} from '@hcengineering/core' } from '@hcengineering/core'
import recruit, { ApplicantMatch } from '@hcengineering/recruit' import recruit, { ApplicantMatch } from '@hcengineering/recruit'
import type { AsyncTriggerControl } from '@hcengineering/server-core' import type { TriggerControl } from '@hcengineering/server-core'
import got from 'got' import got from 'got'
import { convert } from 'html-to-text' import { convert } from 'html-to-text'
import { chunks } from './encoder/encoder' import { chunks } from './encoder/encoder'
@ -111,7 +111,7 @@ async function performCompletion (
/** /**
* @public * @public
*/ */
export async function AsyncOnGPTRequest (tx: Tx, tc: AsyncTriggerControl): Promise<Tx[]> { export async function AsyncOnGPTRequest (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) const actualTx = TxProcessor.extractTx(tx)
if (tc.hierarchy.isDerived(actualTx._class, core.class.TxCUD) && actualTx.modifiedBy !== openai.account.GPT) { if (tc.hierarchy.isDerived(actualTx._class, core.class.TxCUD) && actualTx.modifiedBy !== openai.account.GPT) {
@ -127,7 +127,7 @@ export async function AsyncOnGPTRequest (tx: Tx, tc: AsyncTriggerControl): Promi
return [] return []
} }
async function handleComment (tx: Tx, tc: AsyncTriggerControl): Promise<Tx[]> { async function handleComment (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) const actualTx = TxProcessor.extractTx(tx)
const cud: TxCUD<Doc> = actualTx as TxCUD<Doc> const cud: TxCUD<Doc> = actualTx as TxCUD<Doc>
@ -269,7 +269,7 @@ async function summarizeVacancy (config: OpenAIConfiguration, chunks: string[],
return getText(await performCompletion(candidateSummaryRequest, options, config, maxLen)) ?? chunks[0] return getText(await performCompletion(candidateSummaryRequest, options, config, maxLen)) ?? chunks[0]
} }
async function handleApplicantMatch (tx: Tx, tc: AsyncTriggerControl): Promise<Tx[]> { async function handleApplicantMatch (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
const [config] = await tc.findAll(openai.class.OpenAIConfiguration, {}) const [config] = await tc.findAll(openai.class.OpenAIConfiguration, {})
if (!(config?.enabled ?? false)) { if (!(config?.enabled ?? false)) {

View File

@ -500,18 +500,27 @@ export async function restore (
let idx: number | undefined let idx: number | undefined
let loaded = 0 let loaded = 0
let last = 0 let last = 0
let el = 0
let chunks = 0
while (true) { while (true) {
const st = Date.now()
const it = await connection.loadChunk(c, idx) const it = await connection.loadChunk(c, idx)
chunks++
idx = it.idx idx = it.idx
el += Date.now() - st
for (const [_id, hash] of Object.entries(it.docs)) { for (const [_id, hash] of Object.entries(it.docs)) {
serverChangeset.set(_id as Ref<Doc>, hash) serverChangeset.set(_id as Ref<Doc>, hash)
loaded++ loaded++
} }
const mr = Math.round(loaded / 10000) const mr = Math.round(loaded / 10000)
if (mr !== last) { if (mr !== last) {
last = mr last = mr
console.log(' loaded', loaded) console.log(' loaded from server', loaded, el, chunks)
el = 0
chunks = 0
} }
if (it.finished) { if (it.finished) {
break break
@ -649,7 +658,10 @@ export async function restore (
await sendChunk(undefined, 0) await sendChunk(undefined, 0)
if (docsToRemove.length > 0 && merge !== true) { if (docsToRemove.length > 0 && merge !== true) {
console.log('cleanup', docsToRemove.length) console.log('cleanup', docsToRemove.length)
await connection.clean(c, docsToRemove) while (docsToRemove.length > 0) {
const part = docsToRemove.splice(0, 10000)
await connection.clean(c, part)
}
} }
} }
} finally { } finally {

View File

@ -18,7 +18,7 @@ import type { Metadata, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import type { Class, Ref, Space } from '@hcengineering/core' import type { Class, Ref, Space } from '@hcengineering/core'
import type { AsyncTrigger, AsyncTriggerState, ObjectDDParticipant, Trigger } from './types' import type { ObjectDDParticipant, Trigger } from './types'
/** /**
* @public * @public
@ -30,9 +30,7 @@ export const serverCoreId = 'server-core' as Plugin
*/ */
const serverCore = plugin(serverCoreId, { const serverCore = plugin(serverCoreId, {
class: { class: {
Trigger: '' as Ref<Class<Trigger>>, Trigger: '' as Ref<Class<Trigger>>
AsyncTrigger: '' as Ref<Class<AsyncTrigger>>,
AsyncTriggerState: '' as Ref<Class<AsyncTriggerState>>
}, },
mixin: { mixin: {
ObjectDDParticipant: '' as Ref<ObjectDDParticipant> ObjectDDParticipant: '' as Ref<ObjectDDParticipant>

View File

@ -1,133 +0,0 @@
import core, {
Class,
Doc,
Hierarchy,
MeasureContext,
ModelDb,
Ref,
ServerStorage,
Tx,
TxCUD,
TxFactory,
TxProcessor
} from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import plugin from '../plugin'
import { AsyncTrigger, AsyncTriggerControl, AsyncTriggerFunc } from '../types'
/**
* @public
*/
export class AsyncTriggerProcessor {
canceling: boolean = false
processing: Promise<void> | undefined
triggers: AsyncTrigger[] = []
classes: Ref<Class<Doc>>[] = []
factory = new TxFactory(core.account.System, true)
functions: AsyncTriggerFunc[] = []
trigger = (): void => {}
control: AsyncTriggerControl
constructor (
readonly model: ModelDb,
readonly hierarchy: Hierarchy,
readonly storage: ServerStorage,
readonly metrics: MeasureContext
) {
this.control = {
hierarchy: this.hierarchy,
modelDb: this.model,
txFactory: this.factory,
findAll: async (_class, query, options) => {
return await this.storage.findAll(this.metrics, _class, query, options)
},
apply: async (tx: Tx[], broadcast: boolean, updateTx: boolean): Promise<void> => {
await this.storage.apply(this.metrics, tx, broadcast, updateTx)
}
}
}
async cancel (): Promise<void> {
this.canceling = true
await this.processing
}
async start (): Promise<void> {
await this.updateTriggers()
this.processing = this.doProcessing()
}
async updateTriggers (): Promise<void> {
try {
this.triggers = await this.model.findAll(plugin.class.AsyncTrigger, {})
this.classes = this.triggers.reduce<Ref<Class<Doc>>[]>((arr, it) => arr.concat(it.classes), [])
this.functions = await Promise.all(this.triggers.map(async (trigger) => await getResource(trigger.trigger)))
} catch (err: any) {
console.error(err)
}
}
async tx (tx: Tx[]): Promise<void> {
const result: Tx[] = []
for (const _tx of tx) {
const actualTx = TxProcessor.extractTx(_tx)
if (
this.hierarchy.isDerived(actualTx._class, core.class.TxCUD) &&
this.hierarchy.isDerived(_tx._class, core.class.TxCUD)
) {
const cud = actualTx as TxCUD<Doc>
if (this.classes.some((it) => this.hierarchy.isDerived(cud.objectClass, it))) {
// We need processing
result.push(
this.factory.createTxCreateDoc(plugin.class.AsyncTriggerState, plugin.space.TriggerState, {
tx: _tx as TxCUD<Doc>,
message: 'Processing...'
})
)
}
}
}
if (result.length > 0) {
await this.storage.apply(this.metrics, result, false, false)
this.processing = this.doProcessing()
}
}
private async doProcessing (): Promise<void> {
while (!this.canceling) {
const docs = await this.storage.findAll(this.metrics, plugin.class.AsyncTriggerState, {}, { limit: 10 })
if (docs.length === 0) {
return
}
for (const doc of docs) {
const result: Tx[] = []
if (this.canceling) {
break
}
try {
for (const f of this.functions) {
result.push(...(await f(doc.tx, this.control)))
}
} catch (err: any) {
console.error(err)
}
await this.storage.apply(
this.metrics,
[this.factory.createTxRemoveDoc(doc._class, doc.space, doc._id)],
false,
false
)
await this.storage.apply(this.metrics, result, true, false)
}
}
}
}

View File

@ -58,7 +58,6 @@ import { FullTextIndex } from './fulltext'
import { FullTextIndexPipeline } from './indexer' import { FullTextIndexPipeline } from './indexer'
import { FullTextPipelineStage } from './indexer/types' import { FullTextPipelineStage } from './indexer/types'
import serverCore from './plugin' import serverCore from './plugin'
import { AsyncTriggerProcessor } from './processor'
import { Triggers } from './triggers' import { Triggers } from './triggers'
import type { import type {
ContentAdapterFactory, ContentAdapterFactory,
@ -68,7 +67,7 @@ import type {
ObjectDDParticipant, ObjectDDParticipant,
TriggerControl TriggerControl
} from './types' } from './types'
import { createCacheFindAll } from './utils' import { createFindAll } from './utils'
/** /**
* @public * @public
@ -104,7 +103,6 @@ export interface DbConfiguration {
class TServerStorage implements ServerStorage { class TServerStorage implements ServerStorage {
private readonly fulltext: FullTextIndex private readonly fulltext: FullTextIndex
hierarchy: Hierarchy hierarchy: Hierarchy
triggerProcessor: AsyncTriggerProcessor
scopes = new Map<string, Promise<any>>() scopes = new Map<string, Promise<any>>()
@ -124,15 +122,11 @@ class TServerStorage implements ServerStorage {
) { ) {
this.hierarchy = hierarchy this.hierarchy = hierarchy
this.fulltext = indexFactory(this) this.fulltext = indexFactory(this)
this.triggerProcessor = new AsyncTriggerProcessor(modelDb, hierarchy, this, metrics.newChild('triggers', {}))
void this.triggerProcessor.start()
} }
async close (): Promise<void> { async close (): Promise<void> {
console.timeLog(this.workspace.name, 'closing') console.timeLog(this.workspace.name, 'closing')
await this.fulltext.close() await this.fulltext.close()
console.timeLog(this.workspace.name, 'closing triggers')
await this.triggerProcessor.cancel()
console.timeLog(this.workspace.name, 'closing adapters') console.timeLog(this.workspace.name, 'closing adapters')
for (const o of this.adapters.values()) { for (const o of this.adapters.values()) {
await o.close() await o.close()
@ -525,13 +519,15 @@ class TServerStorage implements ServerStorage {
}, },
findAll: fAll(ctx), findAll: fAll(ctx),
modelDb: this.modelDb, modelDb: this.modelDb,
hierarchy: this.hierarchy hierarchy: this.hierarchy,
apply: async (tx, broadcast) => {
await this.apply(ctx, tx, broadcast)
}
} }
const triggers = await ctx.with('process-triggers', {}, async (ctx) => { const triggers = await ctx.with('process-triggers', {}, async (ctx) => {
const result: Tx[] = [] const result: Tx[] = []
for (const tx of txes) { for (const tx of txes) {
result.push(...(await this.triggers.apply(tx.modifiedBy, tx, triggerControl))) result.push(...(await this.triggers.apply(tx.modifiedBy, tx, triggerControl)))
await ctx.with('async-triggers', {}, (ctx) => this.triggerProcessor.tx([tx]))
} }
return result return result
}) })
@ -597,26 +593,14 @@ class TServerStorage implements ServerStorage {
return { passed, onEnd } return { passed, onEnd }
} }
async apply (ctx: MeasureContext, tx: Tx[], broadcast: boolean, updateTx: boolean): Promise<Tx[]> { async apply (ctx: MeasureContext, tx: Tx[], broadcast: boolean): Promise<Tx[]> {
const triggerFx = new Effects() const triggerFx = new Effects()
const cacheFind = createCacheFindAll(this) const _findAll = createFindAll(this)
const txToStore = tx.filter( const txToStore = tx.filter(
(it) => it.space !== core.space.DerivedTx && !this.hierarchy.isDerived(it._class, core.class.TxApplyIf) (it) => it.space !== core.space.DerivedTx && !this.hierarchy.isDerived(it._class, core.class.TxApplyIf)
) )
if (updateTx) {
const ops = new Map(
tx
.filter((it) => it._class === core.class.TxUpdateDoc)
.map((it) => [(it as TxUpdateDoc<Tx>).objectId, (it as TxUpdateDoc<Tx>).operations])
)
if (ops.size > 0) {
await ctx.with('domain-tx-update', {}, async () => await this.getAdapter(DOMAIN_TX).update(DOMAIN_TX, ops))
}
} else {
await ctx.with('domain-tx', {}, async () => await this.getAdapter(DOMAIN_TX).tx(...txToStore)) await ctx.with('domain-tx', {}, async () => await this.getAdapter(DOMAIN_TX).tx(...txToStore))
}
const removedMap = new Map<Ref<Doc>, Doc>() const removedMap = new Map<Ref<Doc>, Doc>()
await ctx.with('apply', {}, (ctx) => this.routeTx(ctx, removedMap, ...tx)) await ctx.with('apply', {}, (ctx) => this.routeTx(ctx, removedMap, ...tx))
@ -626,7 +610,7 @@ class TServerStorage implements ServerStorage {
this.options?.broadcast?.(tx) this.options?.broadcast?.(tx)
} }
// invoke triggers and store derived objects // invoke triggers and store derived objects
const derived = await this.processDerived(ctx, tx, triggerFx, cacheFind, removedMap) const derived = await this.processDerived(ctx, tx, triggerFx, _findAll, removedMap)
// index object // index object
for (const _tx of tx) { for (const _tx of tx) {
@ -641,13 +625,16 @@ class TServerStorage implements ServerStorage {
for (const fx of triggerFx.effects) { for (const fx of triggerFx.effects) {
await fx() await fx()
} }
if (broadcast && derived.length > 0) {
this.options?.broadcast?.(derived)
}
return [...tx, ...derived] return [...tx, ...derived]
} }
async tx (ctx: MeasureContext, tx: Tx): Promise<[TxResult, Tx[]]> { async tx (ctx: MeasureContext, tx: Tx): Promise<[TxResult, Tx[]]> {
// store tx // store tx
const _class = txClass(tx) const _class = txClass(tx)
const cacheFind = createCacheFindAll(this) const _findAll = createFindAll(this)
const objClass = txObjectClass(tx) const objClass = txObjectClass(tx)
const removedDocs = new Map<Ref<Doc>, Doc>() const removedDocs = new Map<Ref<Doc>, Doc>()
return await ctx.with('tx', { _class, objClass }, async (ctx) => { return await ctx.with('tx', { _class, objClass }, async (ctx) => {
@ -673,7 +660,7 @@ class TServerStorage implements ServerStorage {
const applyIf = tx as TxApplyIf const applyIf = tx as TxApplyIf
// Wait for scope promise if found // Wait for scope promise if found
let passed: boolean let passed: boolean
;({ passed, onEnd } = await this.verifyApplyIf(ctx, applyIf, cacheFind)) ;({ passed, onEnd } = await this.verifyApplyIf(ctx, applyIf, _findAll))
result = passed result = passed
if (passed) { if (passed) {
// Store apply if transaction's if required // Store apply if transaction's if required
@ -681,13 +668,13 @@ class TServerStorage implements ServerStorage {
const atx = await this.getAdapter(DOMAIN_TX) const atx = await this.getAdapter(DOMAIN_TX)
await atx.tx(...applyIf.txes) await atx.tx(...applyIf.txes)
}) })
derived = await this.processDerivedTxes(applyIf.txes, ctx, triggerFx, cacheFind, removedDocs) derived = await this.processDerivedTxes(applyIf.txes, ctx, triggerFx, _findAll, removedDocs)
} }
} else { } else {
// store object // store object
result = await ctx.with('route-tx', { _class, objClass }, (ctx) => this.routeTx(ctx, removedDocs, tx)) result = await ctx.with('route-tx', { _class, objClass }, (ctx) => this.routeTx(ctx, removedDocs, tx))
// invoke triggers and store derived objects // invoke triggers and store derived objects
derived = await this.processDerived(ctx, [tx], triggerFx, cacheFind, removedDocs) derived = await this.processDerived(ctx, [tx], triggerFx, _findAll, removedDocs)
} }
// index object // index object

View File

@ -14,8 +14,18 @@
// limitations under the License. // limitations under the License.
// //
import type { Tx, Doc, TxCreateDoc, Ref, Account, TxCollectionCUD, AttachedDoc } from '@hcengineering/core' import core, {
import core, { TxFactory } from '@hcengineering/core' Tx,
Doc,
TxCreateDoc,
Ref,
Account,
TxCollectionCUD,
AttachedDoc,
DocumentQuery,
matchQuery,
TxFactory
} from '@hcengineering/core'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import type { Trigger, TriggerFunc, TriggerControl } from './types' import type { Trigger, TriggerFunc, TriggerControl } from './types'
@ -26,7 +36,7 @@ import serverCore from './plugin'
* @public * @public
*/ */
export class Triggers { export class Triggers {
private readonly triggers: TriggerFunc[] = [] private readonly triggers: [DocumentQuery<Tx> | undefined, TriggerFunc][] = []
async tx (tx: Tx): Promise<void> { async tx (tx: Tx): Promise<void> {
if (tx._class === core.class.TxCollectionCUD) { if (tx._class === core.class.TxCollectionCUD) {
@ -36,14 +46,18 @@ export class Triggers {
const createTx = tx as TxCreateDoc<Doc> const createTx = tx as TxCreateDoc<Doc>
if (createTx.objectClass === serverCore.class.Trigger) { if (createTx.objectClass === serverCore.class.Trigger) {
const trigger = (createTx as TxCreateDoc<Trigger>).attributes.trigger const trigger = (createTx as TxCreateDoc<Trigger>).attributes.trigger
const match = (createTx as TxCreateDoc<Trigger>).attributes.txMatch
const func = await getResource(trigger) const func = await getResource(trigger)
this.triggers.push(func) this.triggers.push([match, func])
} }
} }
} }
async apply (account: Ref<Account>, tx: Tx, ctrl: Omit<TriggerControl, 'txFactory'>): Promise<Tx[]> { async apply (account: Ref<Account>, tx: Tx, ctrl: Omit<TriggerControl, 'txFactory'>): Promise<Tx[]> {
const derived = this.triggers.map((trigger) => trigger(tx, { ...ctrl, txFactory: new TxFactory(account, true) })) const control = { ...ctrl, txFactory: new TxFactory(account, true) }
const derived = this.triggers
.filter(([query]) => query === undefined || matchQuery([tx], query, core.class.Tx, control.hierarchy).length > 0)
.map(([, trigger]) => trigger(tx, control))
const result = await Promise.all(derived) const result = await Promise.all(derived)
return result.flatMap((x) => x) return result.flatMap((x) => x)
} }

View File

@ -32,7 +32,6 @@ import {
Storage, Storage,
Timestamp, Timestamp,
Tx, Tx,
TxCUD,
TxFactory, TxFactory,
TxResult, TxResult,
WorkspaceId WorkspaceId
@ -104,6 +103,9 @@ export interface TriggerControl {
// Later can be replaced with generic one with bucket encapsulated inside. // Later can be replaced with generic one with bucket encapsulated inside.
storageFx: (f: (adapter: MinioService, workspaceId: WorkspaceId) => Promise<void>) => void storageFx: (f: (adapter: MinioService, workspaceId: WorkspaceId) => Promise<void>) => void
fx: (f: () => Promise<void>) => void fx: (f: () => Promise<void>) => void
// Bulk operations in case trigger require some
apply: (tx: Tx[], broadcast: boolean) => Promise<void>
} }
/** /**
@ -111,42 +113,14 @@ export interface TriggerControl {
*/ */
export type TriggerFunc = (tx: Tx, ctrl: TriggerControl) => Promise<Tx[]> export type TriggerFunc = (tx: Tx, ctrl: TriggerControl) => Promise<Tx[]>
/**
* @public
*/
export interface AsyncTriggerControl {
txFactory: TxFactory
findAll: Storage['findAll']
apply: (tx: Tx[], broadcast: boolean, updateTx: boolean) => Promise<void>
hierarchy: Hierarchy
modelDb: ModelDb
}
/**
* @public
*/
export type AsyncTriggerFunc = (tx: Tx, ctrl: AsyncTriggerControl) => Promise<Tx[]>
/** /**
* @public * @public
*/ */
export interface Trigger extends Doc { export interface Trigger extends Doc {
trigger: Resource<TriggerFunc> trigger: Resource<TriggerFunc>
}
/** // We should match transaction
* @public txMatch?: DocumentQuery<Tx>
*/
export interface AsyncTrigger extends Doc {
trigger: Resource<AsyncTriggerFunc>
classes: Ref<Class<Doc>>[]
}
/**
* @public
*/
export interface AsyncTriggerState extends Doc {
tx: TxCUD<Doc>
message: string
} }
/** /**

View File

@ -12,23 +12,13 @@ import {
/** /**
* @public * @public
*/ */
export function createCacheFindAll (storage: ServerStorage): ServerStorage['findAll'] { export function createFindAll (storage: ServerStorage): ServerStorage['findAll'] {
// We will cache all queries for same objects for all derived data checks.
const queryCache = new Map<string, FindResult<Doc>>()
return async <T extends Doc>( return async <T extends Doc>(
ctx: MeasureContext, ctx: MeasureContext,
clazz: Ref<Class<T>>, clazz: Ref<Class<T>>,
query: DocumentQuery<T>, query: DocumentQuery<T>,
options?: FindOptions<T> options?: FindOptions<T>
): Promise<FindResult<T>> => { ): Promise<FindResult<T>> => {
const key = JSON.stringify(clazz) + JSON.stringify(query) + JSON.stringify(options) return await storage.findAll(ctx, clazz, query, options)
let cacheResult = queryCache.get(key)
if (cacheResult !== undefined) {
return cacheResult as FindResult<T>
}
cacheResult = await storage.findAll(ctx, clazz, query, options)
queryCache.set(key, cacheResult)
return storage.hierarchy.clone(cacheResult) as FindResult<T>
} }
} }

View File

@ -33,7 +33,6 @@ import core, {
} from '@hcengineering/core' } from '@hcengineering/core'
import type { IntlString, Plugin } from '@hcengineering/platform' import type { IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import server from '@hcengineering/server-core'
export const txFactory = new TxFactory(core.account.System) export const txFactory = new TxFactory(core.account.System)
@ -126,22 +125,7 @@ export function genMinModel (): TxCUD<Doc>[] {
domain: DOMAIN_DOC_INDEX_STATE domain: DOMAIN_DOC_INDEX_STATE
}) })
) )
txes.push(
createClass(server.class.AsyncTrigger, {
label: 'AsyncTrigger' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_MODEL
})
)
txes.push(
createClass(server.class.AsyncTriggerState, {
label: 'AsyncTriggerState' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_DOC_INDEX_STATE
})
)
txes.push( txes.push(
createClass(core.class.Account, { createClass(core.class.Account, {
label: 'Account' as IntlString, label: 'Account' as IntlString,

View File

@ -542,7 +542,7 @@ abstract class MongoAdapterBase implements DbAdapter {
find (domain: Domain): StorageIterator { find (domain: Domain): StorageIterator {
const coll = this.db.collection<Doc>(domain) const coll = this.db.collection<Doc>(domain)
const iterator = coll.find({}, {}) const iterator = coll.find({}, {}).batchSize(100)
return { return {
next: async () => { next: async () => {

View File

@ -65,7 +65,7 @@ export class BackupClientSession extends ClientSession implements BackupSession
break break
} }
size = size + doc.id.length + doc.hash.length + doc.size size = size + doc.size
docs[doc.id] = doc.hash docs[doc.id] = doc.hash
} }

View File

@ -355,7 +355,7 @@ async function handleRequest<S extends Session> (
} }
}, 30000) }, 30000)
const result = await f.apply(service, params) let result = await f.apply(service, params)
clearTimeout(timeout) clearTimeout(timeout)
clearTimeout(hangTimeout) clearTimeout(hangTimeout)
const resp: Response<any> = { id: request.id, result } const resp: Response<any> = { id: request.id, result }
@ -373,7 +373,11 @@ async function handleRequest<S extends Session> (
diff diff
) )
} }
ws.send(serialize(resp)) const toSend = serialize(resp)
// Clear for gc to make work
resp.result = undefined
result = undefined
ws.send(toSend)
} catch (err: any) { } catch (err: any) {
if (LOGGING_ENABLED) console.error(err) if (LOGGING_ENABLED) console.error(err)
clearTimeout(timeout) clearTimeout(timeout)
@ -427,7 +431,7 @@ export function start (
}) })
const session = await sessions.addSession(ctx, ws, token, pipelineFactory, productId, sessionId) const session = await sessions.addSession(ctx, ws, token, pipelineFactory, productId, sessionId)
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on('message', async (msg: RawData) => { ws.on('message', (msg: RawData) => {
let msgStr = '' let msgStr = ''
if (typeof msg === 'string') { if (typeof msg === 'string') {
msgStr = msg msgStr = msg
@ -436,7 +440,7 @@ export function start (
} else if (Array.isArray(msg)) { } else if (Array.isArray(msg)) {
msgStr = Buffer.concat(msg).toString() msgStr = Buffer.concat(msg).toString()
} }
await handleRequest(ctx, session, ws, msgStr, token.workspace.name) void handleRequest(ctx, session, ws, msgStr, token.workspace.name)
}) })
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on('close', (code: number, reason: Buffer) => { ws.on('close', (code: number, reason: Buffer) => {