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 {
employee!: Ref<Employee>
name!: string
mergedTo!: Ref<EmployeeAccount>
}
@Model(contact.class.Organizations, core.class.Space)

View File

@ -47,8 +47,11 @@ export function createModel (builder: Builder): void {
trigger: serverContact.trigger.OnChannelUpdate
})
builder.createDoc(serverCore.class.AsyncTrigger, core.space.Model, {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
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.
//
import { Model, Builder } from '@hcengineering/model'
import type { Resource } from '@hcengineering/platform'
import { Builder, Model } from '@hcengineering/model'
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, {
Class,
DOMAIN_MODEL,
Doc,
DocumentQuery,
DOMAIN_DOC_INDEX_STATE,
DOMAIN_MODEL,
FindOptions,
FindResult,
Hierarchy,
Ref,
TxCUD
Ref
} from '@hcengineering/core'
import type { ObjectDDParticipant, Trigger, TriggerFunc } from '@hcengineering/server-core'
import serverCore from '@hcengineering/server-core'
@Model(serverCore.class.Trigger, core.class.Doc, DOMAIN_MODEL)
@ -45,18 +36,6 @@ export class TTrigger extends TDoc implements Trigger {
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)
export class TObjectDDParticipant extends TClass implements ObjectDDParticipant {
collectDocs!: Resource<
@ -73,5 +52,5 @@ export class TObjectDDParticipant extends TClass implements ObjectDDParticipant
}
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 {
builder.createModel(TOpenAIConfiguration)
builder.createDoc(serverCore.class.AsyncTrigger, core.space.Model, {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
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>
) => Promise<FindResult<T>>
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>
}

View File

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

View File

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

View File

@ -1,16 +1,9 @@
<script lang="ts">
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { Account, IdMap, Ref, toIdMap } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
import { Account, IdMap, Ref } from '@hcengineering/core'
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 {
const account = accounts.get(acc as Ref<EmployeeAccount>)
@ -24,6 +17,6 @@
{#each reactionAccounts as acc}
<div>
{getAccName(acc, accounts, $employeeByIdStore)}
{getAccName(acc, $employeeAccountByIdStore, $employeeByIdStore)}
</div>
{/each}

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,12 @@
const query: DocumentQuery<EmployeeAccount> =
isSearch > 0 ? { name: { $like: '%' + search + '%' } } : { _id: { $in: accounts as Ref<EmployeeAccount>[] } }
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(
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 employeesStore = writable<Employee[]>([])
export const employeeAccountByIdStore = writable<IdMap<EmployeeAccount>>(new Map())
export const channelProviders = writable<ChannelProvider[]>([])
function fillStores (): void {
@ -234,6 +237,22 @@ function fillStores (): void {
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)
providerQuery.query(contact.class.ChannelProvider, {}, (res) => channelProviders.set(res))
} else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,15 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { EmployeeAccount } from '@hcengineering/contact'
import { employeeByIdStore, EmployeeBox } from '@hcengineering/contact-resources'
import { EmployeeAccount } from '@hcengineering/contact'
import { EmployeeBox, employeeAccountByIdStore, employeeByIdStore } from '@hcengineering/contact-resources'
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 type { Issue } from '@hcengineering/tracker'
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 ComponentEditor from '../../components/ComponentEditor.svelte'
import SprintEditor from '../../sprints/SprintEditor.svelte'
@ -87,19 +87,10 @@
'blockedBy'
])
const employeeAccountQuery = createQuery()
let account: EmployeeAccount | undefined
$: employee = account && $employeeByIdStore.get(account.employee)
$: employeeAccountQuery.query(
contact.class.EmployeeAccount,
{ _id: issue.createdBy as Ref<EmployeeAccount> },
(res) => {
;[account] = res
},
{ limit: 1 }
)
$: account = $employeeAccountByIdStore.get(issue.createdBy as Ref<EmployeeAccount>)
</script>
<div class="content">

View File

@ -66,7 +66,7 @@ export async function connect (title: string): Promise<Client | undefined> {
void (async () => {
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 reconnectVersionStr = versionToString(newVersion as Version)

View File

@ -41,7 +41,7 @@ import core, {
} from '@hcengineering/core'
import notification, { Collaborators } from '@hcengineering/notification'
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'
/**
@ -110,13 +110,13 @@ export async function OnContactDelete (
}
async function updateAllRefs (
control: AsyncTriggerControl,
control: TriggerControl,
sourceAccount: EmployeeAccount,
targetAccount: EmployeeAccount,
modifiedOn: Timestamp,
modifiedBy: Ref<Account>
): 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.
const reftos = (await control.modelDb.findAll(core.class.Attribute, { 'type._class': core.class.RefTo })).filter(
(it) => {
@ -133,6 +133,9 @@ async function updateAllRefs (
if (to.to === contact.class.Employee) {
const descendants = control.hierarchy.getDescendants(attr.attributeOf)
for (const d of descendants) {
if (control.hierarchy.isDerived(d, core.class.Tx)) {
continue
}
if (control.hierarchy.findDomain(d) !== undefined) {
while (true) {
const values = await control.findAll(d, { [attr.name]: sourceAccount.employee }, { limit: 100 })
@ -144,7 +147,10 @@ async function updateAllRefs (
for (const v of values) {
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)
for (const d of descendants) {
if (control.hierarchy.isDerived(d, core.class.Tx)) {
continue
}
if (control.hierarchy.findDomain(d) !== undefined) {
while (true) {
const values = await control.findAll(d, { [attr.name]: sourceAccount._id }, { limit: 100 })
@ -166,7 +175,10 @@ async function updateAllRefs (
for (const v of values) {
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) {
const descendants = control.hierarchy.getDescendants(attr.attributeOf)
for (const d of descendants) {
if (control.hierarchy.isDerived(d, core.class.Tx)) {
continue
}
if (control.hierarchy.findDomain(d) !== undefined) {
while (true) {
const values = await control.findAll(
@ -195,7 +210,10 @@ async function updateAllRefs (
for (const v of values) {
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)
for (const d of descendants) {
if (control.hierarchy.isDerived(d, core.class.Tx)) {
continue
}
if (control.hierarchy.findDomain(d) !== undefined) {
while (true) {
const values = await control.findAll(d, { [attr.name]: sourceAccount._id }, { limit: 100 })
@ -217,27 +238,30 @@ async function updateAllRefs (
for (const v of values) {
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 builder = new TxBuilder(control.hierarchy, control.modelDb, modifiedBy)
await builder.remove(sourceAccount)
if (employee !== undefined) {
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 []
const target = uTx.operations.mergedTo
const res: Tx[] = []
const attributes = control.hierarchy.getAllAttributes(contact.class.Employee)
@ -245,19 +269,26 @@ async function mergeEmployee (control: AsyncTriggerControl, uTx: TxUpdateDoc<Emp
if (control.hierarchy.isDerived(attribute[1].type._class, core.class.Collection)) {
if (attribute[1]._id === contact.class.Contact + '_channels') continue
const collection = attribute[1].type as Collection<AttachedDoc>
const allAttached = await control.findAll(collection.of, { attachedTo: uTx.objectId })
for (const attached of allAttached) {
const tx = control.txFactory.createTxUpdateDoc(attached._class, attached.space, attached._id, {
attachedTo: target
})
const parent = control.txFactory.createTxCollectionCUD(
attached.attachedToClass,
target,
attached.space,
attached.collection,
tx
)
res.push(parent)
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) {
const tx = control.txFactory.createTxUpdateDoc(attached._class, attached.space, attached._id, {
attachedTo: target
})
const parent = control.txFactory.createTxCollectionCUD(
attached.attachedToClass,
target,
attached.space,
attached.collection,
tx
)
res.push(parent)
}
await control.apply(res, false)
}
}
}
@ -267,32 +298,18 @@ async function mergeEmployee (control: AsyncTriggerControl, uTx: TxUpdateDoc<Emp
)[0]
const newEmployeeAccount = (await control.modelDb.findAll(contact.class.EmployeeAccount, { employee: target }))[0]
if (oldEmployeeAccount === undefined || newEmployeeAccount === undefined) return res
const accountTxes = await updateAllRefs(
control,
oldEmployeeAccount,
newEmployeeAccount,
uTx.modifiedOn,
uTx.modifiedBy
)
res.push(...accountTxes)
return res
if (oldEmployeeAccount === undefined || newEmployeeAccount === undefined) {
return []
}
return await updateAllRefs(control, oldEmployeeAccount, newEmployeeAccount, uTx.modifiedOn, uTx.modifiedBy)
}
/**
* @public
*/
export async function OnEmployeeUpdate (tx: Tx, control: AsyncTriggerControl): Promise<Tx[]> {
if (tx._class !== core.class.TxUpdateDoc) {
return []
}
export async function OnEmployeeUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const uTx = tx as TxUpdateDoc<Employee>
if (!control.hierarchy.isDerived(uTx.objectClass, contact.class.Employee)) {
return []
}
const result: Tx[] = []
const txes = await mergeEmployee(control, uTx)

View File

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

View File

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

View File

@ -27,7 +27,7 @@ import core, {
TxProcessor
} from '@hcengineering/core'
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 { convert } from 'html-to-text'
import { chunks } from './encoder/encoder'
@ -111,7 +111,7 @@ async function performCompletion (
/**
* @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)
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 []
}
async function handleComment (tx: Tx, tc: AsyncTriggerControl): Promise<Tx[]> {
async function handleComment (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
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]
}
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, {})
if (!(config?.enabled ?? false)) {

View File

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

View File

@ -18,7 +18,7 @@ import type { Metadata, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Class, Ref, Space } from '@hcengineering/core'
import type { AsyncTrigger, AsyncTriggerState, ObjectDDParticipant, Trigger } from './types'
import type { ObjectDDParticipant, Trigger } from './types'
/**
* @public
@ -30,9 +30,7 @@ export const serverCoreId = 'server-core' as Plugin
*/
const serverCore = plugin(serverCoreId, {
class: {
Trigger: '' as Ref<Class<Trigger>>,
AsyncTrigger: '' as Ref<Class<AsyncTrigger>>,
AsyncTriggerState: '' as Ref<Class<AsyncTriggerState>>
Trigger: '' as Ref<Class<Trigger>>
},
mixin: {
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 { FullTextPipelineStage } from './indexer/types'
import serverCore from './plugin'
import { AsyncTriggerProcessor } from './processor'
import { Triggers } from './triggers'
import type {
ContentAdapterFactory,
@ -68,7 +67,7 @@ import type {
ObjectDDParticipant,
TriggerControl
} from './types'
import { createCacheFindAll } from './utils'
import { createFindAll } from './utils'
/**
* @public
@ -104,7 +103,6 @@ export interface DbConfiguration {
class TServerStorage implements ServerStorage {
private readonly fulltext: FullTextIndex
hierarchy: Hierarchy
triggerProcessor: AsyncTriggerProcessor
scopes = new Map<string, Promise<any>>()
@ -124,15 +122,11 @@ class TServerStorage implements ServerStorage {
) {
this.hierarchy = hierarchy
this.fulltext = indexFactory(this)
this.triggerProcessor = new AsyncTriggerProcessor(modelDb, hierarchy, this, metrics.newChild('triggers', {}))
void this.triggerProcessor.start()
}
async close (): Promise<void> {
console.timeLog(this.workspace.name, 'closing')
await this.fulltext.close()
console.timeLog(this.workspace.name, 'closing triggers')
await this.triggerProcessor.cancel()
console.timeLog(this.workspace.name, 'closing adapters')
for (const o of this.adapters.values()) {
await o.close()
@ -525,13 +519,15 @@ class TServerStorage implements ServerStorage {
},
findAll: fAll(ctx),
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 result: Tx[] = []
for (const tx of txes) {
result.push(...(await this.triggers.apply(tx.modifiedBy, tx, triggerControl)))
await ctx.with('async-triggers', {}, (ctx) => this.triggerProcessor.tx([tx]))
}
return result
})
@ -597,26 +593,14 @@ class TServerStorage implements ServerStorage {
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 cacheFind = createCacheFindAll(this)
const _findAll = createFindAll(this)
const txToStore = tx.filter(
(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>()
await ctx.with('apply', {}, (ctx) => this.routeTx(ctx, removedMap, ...tx))
@ -626,7 +610,7 @@ class TServerStorage implements ServerStorage {
this.options?.broadcast?.(tx)
}
// 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
for (const _tx of tx) {
@ -641,13 +625,16 @@ class TServerStorage implements ServerStorage {
for (const fx of triggerFx.effects) {
await fx()
}
if (broadcast && derived.length > 0) {
this.options?.broadcast?.(derived)
}
return [...tx, ...derived]
}
async tx (ctx: MeasureContext, tx: Tx): Promise<[TxResult, Tx[]]> {
// store tx
const _class = txClass(tx)
const cacheFind = createCacheFindAll(this)
const _findAll = createFindAll(this)
const objClass = txObjectClass(tx)
const removedDocs = new Map<Ref<Doc>, Doc>()
return await ctx.with('tx', { _class, objClass }, async (ctx) => {
@ -673,7 +660,7 @@ class TServerStorage implements ServerStorage {
const applyIf = tx as TxApplyIf
// Wait for scope promise if found
let passed: boolean
;({ passed, onEnd } = await this.verifyApplyIf(ctx, applyIf, cacheFind))
;({ passed, onEnd } = await this.verifyApplyIf(ctx, applyIf, _findAll))
result = passed
if (passed) {
// Store apply if transaction's if required
@ -681,13 +668,13 @@ class TServerStorage implements ServerStorage {
const atx = await this.getAdapter(DOMAIN_TX)
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 {
// store object
result = await ctx.with('route-tx', { _class, objClass }, (ctx) => this.routeTx(ctx, removedDocs, tx))
// 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

View File

@ -14,8 +14,18 @@
// limitations under the License.
//
import type { Tx, Doc, TxCreateDoc, Ref, Account, TxCollectionCUD, AttachedDoc } from '@hcengineering/core'
import core, { TxFactory } from '@hcengineering/core'
import core, {
Tx,
Doc,
TxCreateDoc,
Ref,
Account,
TxCollectionCUD,
AttachedDoc,
DocumentQuery,
matchQuery,
TxFactory
} from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import type { Trigger, TriggerFunc, TriggerControl } from './types'
@ -26,7 +36,7 @@ import serverCore from './plugin'
* @public
*/
export class Triggers {
private readonly triggers: TriggerFunc[] = []
private readonly triggers: [DocumentQuery<Tx> | undefined, TriggerFunc][] = []
async tx (tx: Tx): Promise<void> {
if (tx._class === core.class.TxCollectionCUD) {
@ -36,14 +46,18 @@ export class Triggers {
const createTx = tx as TxCreateDoc<Doc>
if (createTx.objectClass === serverCore.class.Trigger) {
const trigger = (createTx as TxCreateDoc<Trigger>).attributes.trigger
const match = (createTx as TxCreateDoc<Trigger>).attributes.txMatch
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[]> {
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)
return result.flatMap((x) => x)
}

View File

@ -32,7 +32,6 @@ import {
Storage,
Timestamp,
Tx,
TxCUD,
TxFactory,
TxResult,
WorkspaceId
@ -104,6 +103,9 @@ export interface TriggerControl {
// Later can be replaced with generic one with bucket encapsulated inside.
storageFx: (f: (adapter: MinioService, workspaceId: WorkspaceId) => 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[]>
/**
* @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
*/
export interface Trigger extends Doc {
trigger: Resource<TriggerFunc>
}
/**
* @public
*/
export interface AsyncTrigger extends Doc {
trigger: Resource<AsyncTriggerFunc>
classes: Ref<Class<Doc>>[]
}
/**
* @public
*/
export interface AsyncTriggerState extends Doc {
tx: TxCUD<Doc>
message: string
// We should match transaction
txMatch?: DocumentQuery<Tx>
}
/**

View File

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

View File

@ -33,7 +33,6 @@ import core, {
} from '@hcengineering/core'
import type { IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import server from '@hcengineering/server-core'
export const txFactory = new TxFactory(core.account.System)
@ -126,22 +125,7 @@ export function genMinModel (): TxCUD<Doc>[] {
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(
createClass(core.class.Account, {
label: 'Account' as IntlString,

View File

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

View File

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

View File

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