// // Copyright © 2023 Hardcore Engineering Inc. // import chunter from '@hcengineering/chunter' import contact, { PersonAccount } from '@hcengineering/contact' import core, { Doc, DocumentUpdate, Hierarchy, Ref, Space, Storage, Tx, TxCUD, TxProcessor, TxUpdateDoc, systemAccountEmail, type Class, type TxMixin } from '@hcengineering/core' import github, { DocSyncInfo, GithubProject } from '@hcengineering/github' import { TriggerControl } from '@hcengineering/server-core' import time, { ToDo } from '@hcengineering/time' import tracker from '@hcengineering/tracker' /** * @public */ export async function OnGithubBroadcast (txes: Tx[], control: TriggerControl): Promise { // Enhance broadcast to send DocSyncInfo change only to system account. control.ctx.contextData.broadcast.targets.github = (it) => { if (TxProcessor.isExtendsCUD(it._class)) { if ((it as TxCUD).objectClass === github.class.DocSyncInfo) { return [systemAccountEmail] } } } return [] } /** * @public */ export async function OnProjectChanges (txes: Tx[], control: TriggerControl): Promise { // Enhance broadcast to send DocSyncInfo change only to system account. await OnGithubBroadcast(txes, control) const result: Tx[] = [] const cache = new Map() const toApply: Tx[] = [] for (const ltx of txes) { if (ltx._class === core.class.TxMixin && (ltx as TxMixin).mixin === github.mixin.GithubIssue) { const mix = ltx as TxMixin // Do not spend time to wait for trigger processing await updateDocSyncInfo(control, ltx, mix.objectSpace, mix, cache, toApply) continue } if (TxProcessor.isExtendsCUD(ltx._class)) { const cud = ltx as TxCUD let space: Ref = cud.objectSpace if (cud._class === core.class.TxUpdateDoc) { const upd = cud as TxUpdateDoc if (upd.operations.space != null) { space = upd.operations.space } } if (isDocSyncUpdateRequired(control.hierarchy, cud)) { await updateDocSyncInfo(control, ltx, space, cud, cache, toApply) } if (control.hierarchy.isDerived(cud.objectClass, time.class.ToDo)) { if (cud.attachedToClass !== undefined && cud.attachedTo !== undefined) { if (control.hierarchy.isDerived(cud.attachedToClass, github.class.GithubPullRequest)) { // Ok we got todo change for pull request, let's mark it for sync. result.push( control.txFactory.createTxUpdateDoc( github.class.DocSyncInfo, ltx.objectSpace, cud.attachedTo as Ref, { needSync: '' } ) ) } } } } } if (toApply.length > 0) { await control.apply(control.ctx, toApply) } return result } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { OnProjectChanges, OnGithubBroadcast }, functions: { TodoDoneTester } }) async function TodoDoneTester ( client: { findAll: Storage['findAll'] hierarchy: Hierarchy }, todo: ToDo ): Promise { if (client.hierarchy.hasMixin(todo, github.mixin.GithubTodo)) { return false } return true } async function updateDocSyncInfo ( control: TriggerControl, tx: Tx, space: Ref, cud: { _class: Ref> objectId: Ref objectClass: Ref> }, cache: Map, toApply: Tx[] ): Promise { const checkTx = (tx: Tx): boolean => control.hierarchy.isDerived(tx._class, core.class.TxCUD) && (tx as TxCUD).objectClass === github.class.DocSyncInfo && (tx as TxCUD).objectId === cud.objectId const txes = [...control.txes, ...control.ctx.contextData.broadcast.txes, ...toApply] // Check already captured Txes for (const i of txes) { if (checkTx(i)) { // We have sync doc create request already. return } } const [account] = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: tx.modifiedBy as Ref }) // Do not modify state if is modified by github service. if (account === undefined) { return } const projects = (cache.get('projects') as GithubProject[]) ?? (await control.queryFind(control.ctx, github.mixin.GithubProject, {}, { projection: { _id: 1 } })) cache.set('projects', projects) if (projects.some((it) => it._id === (space as Ref))) { const sdoc = (cache.get(cud.objectId) as DocSyncInfo) ?? ( await control.findAll(control.ctx, github.class.DocSyncInfo, { _id: cud.objectId as Ref }) ).shift() // We need to check if sync doc is already exists. if (sdoc === undefined) { // Created by non github integration // We need to create the doc sync info createSyncDoc(control, cud, tx, space, toApply) } else { cache.set(cud.objectId, sdoc) // We need to create the doc sync info updateSyncDoc(control, cud, space, sdoc, toApply) } } } function isDocSyncUpdateRequired (h: Hierarchy, coll: TxCUD): boolean { return ( h.isDerived(coll.objectClass, tracker.class.Issue) || h.isDerived(coll.objectClass, chunter.class.ChatMessage) || h.isDerived(coll.objectClass, github.class.GithubReviewComment) || h.isDerived(coll.objectClass, github.class.GithubReview) || h.isDerived(coll.objectClass, github.class.GithubReviewThread) || h.isDerived(coll.objectClass, tracker.class.Milestone) ) } function updateSyncDoc ( control: TriggerControl, cud: { _class: Ref> objectId: Ref objectClass: Ref> }, space: Ref, info: DocSyncInfo, toApply: Tx[] ): void { const data: DocumentUpdate = cud._class === core.class.TxRemoveDoc ? { needSync: '', deleted: true } : { needSync: '' } if (info.space !== space) { data.externalVersion = '#' // We need to put this one to handle new documents.) data.space = space } toApply.push( control.txFactory.createTxUpdateDoc( github.class.DocSyncInfo, info.space, cud.objectId as Ref, data ) ) } function createSyncDoc ( control: TriggerControl, cud: { _class: Ref> objectId: Ref objectClass: Ref> }, tx: Tx, space: Ref, toApply: Tx[] ): void { const data: DocumentUpdate = { url: '', githubNumber: 0, repository: null, objectClass: cud.objectClass, externalVersion: '#', // We need to put this one to handle new documents. needSync: '', derivedVersion: '' } if ((tx as TxCUD).attachedTo !== undefined) { // Collection CUD, we could assign attachedTo data.attachedTo = (tx as TxCUD).attachedTo } toApply.push( control.txFactory.createTxCreateDoc( github.class.DocSyncInfo, space, data, cud.objectId as Ref ) ) }