import attachment, { Attachment } from '@hcengineering/attachment' import chunter, { Comment } from '@hcengineering/chunter' import contact, { combineName, EmployeeAccount } from '@hcengineering/contact' import core, { AccountRole, ApplyOperations, AttachedDoc, Class, concatLink, Data, Doc, DocumentUpdate, FindResult, generateId, Hierarchy, Mixin, MixinUpdate, Ref, Space, TxOperations, TxProcessor, WithLookup } from '@hcengineering/core' import tags, { TagElement } from '@hcengineering/tags' import { deepEqual } from 'fast-equals' import { BitrixClient } from './client' import bitrix from './index' import { BitrixActivity, BitrixEntityMapping, BitrixEntityType, BitrixFieldMapping, BitrixSyncDoc, LoginInfo } from './types' import { convert, ConvertResult } from './utils' async function updateDoc (client: ApplyOperations, doc: Doc, raw: Doc | Data): Promise { // We need to update fields if they are different. const documentUpdate: DocumentUpdate = {} for (const [k, v] of Object.entries(raw)) { if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { continue } const dv = (doc as any)[k] if (!deepEqual(dv, v) && dv != null && v != null) { ;(documentUpdate as any)[k] = v } } if (Object.keys(documentUpdate).length > 0) { await client.update(doc, documentUpdate) TxProcessor.applyUpdate(doc, documentUpdate) } return doc } async function updateMixin ( client: ApplyOperations, doc: Doc, raw: Doc | Data, mixin: Ref>> ): Promise { // We need to update fields if they are different. const documentUpdate: MixinUpdate = {} for (const [k, v] of Object.entries(raw)) { if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { continue } const dv = (doc as any)[k] if (!deepEqual(dv, v) && dv != null && v != null) { ;(documentUpdate as any)[k] = v } } if (Object.keys(documentUpdate).length > 0) { await client.updateMixin(doc._id, doc._class, doc.space, mixin, documentUpdate) } return doc } /** * @public */ export async function syncDocument ( client: TxOperations, existing: Doc | undefined, resultDoc: ConvertResult, info: LoginInfo, frontUrl: string, monitor?: (doc: ConvertResult) => void ): Promise { const hierarchy = client.getHierarchy() try { const applyOp = client.apply('bitrix') // const newDoc = existing === undefined existing = await updateMainDoc(applyOp) const mixins = { ...resultDoc.mixins } // Add bitrix sync mixin mixins[bitrix.mixin.BitrixSyncDoc] = { type: resultDoc.document.type, bitrixId: resultDoc.document.bitrixId, rawData: resultDoc.rawData, syncTime: Date.now() } // Check and update mixins await updateMixins(mixins, hierarchy, existing, applyOp, resultDoc.document) // Just create supplier documents, like TagElements. for (const ed of resultDoc.extraDocs) { await applyOp.createDoc( ed._class, ed.space, ed, ed._id, resultDoc.document.modifiedOn, resultDoc.document.modifiedBy ) } // Find all attachemnt documents to existing. const byClass = new Map>, (AttachedDoc & BitrixSyncDoc)[]>() for (const d of resultDoc.extraSync) { byClass.set(d._class, [...(byClass.get(d._class) ?? []), d]) } for (const [cl, vals] of byClass.entries()) { if (applyOp.getHierarchy().isDerived(cl, core.class.AttachedDoc)) { const existingByClass = await client.findAll(cl, { attachedTo: resultDoc.document._id, [bitrix.mixin.BitrixSyncDoc + '.bitrixId']: { $in: vals.map((it) => it.bitrixId) } }) for (const valValue of vals) { const existing = existingByClass.find( (it) => hierarchy.as(it, bitrix.mixin.BitrixSyncDoc).bitrixId === valValue.bitrixId ) await updateAttachedDoc(existing, applyOp, valValue) } } } const existingBlobs = await client.findAll(attachment.class.Attachment, { attachedTo: resultDoc.document._id, [bitrix.mixin.BitrixSyncDoc + '.bitrixId']: { $in: resultDoc.blobs.map((it) => it[0].bitrixId) } }) for (const [ed, op] of resultDoc.blobs) { const existing = existingBlobs.find( (it) => hierarchy.as(it, bitrix.mixin.BitrixSyncDoc).bitrixId === ed.bitrixId ) // For Attachments, just do it once per attachment and assume it is not changed. if (existing === undefined) { const attachmentId: Ref = generateId() try { const edData = await op() if (edData === undefined) { console.error('Failed to retrieve document data', ed.name) continue } const data = new FormData() data.append('file', edData) const resp = await fetch(concatLink(frontUrl, '/files'), { method: 'POST', headers: { Authorization: 'Bearer ' + info.token }, body: data }) if (resp.status === 200) { const uuid = await resp.text() await applyOp.addCollection( attachment.class.Attachment, resultDoc.document.space, resultDoc.document._id, resultDoc.document._class, 'attachments', { file: uuid, lastModified: edData.lastModified, name: edData.name, size: edData.size, type: edData.type }, attachmentId, resultDoc.document.modifiedOn, resultDoc.document.modifiedBy ) } } catch (err: any) { console.error(err) } } } await applyOp.commit() } catch (err: any) { console.error(err) } monitor?.(resultDoc) async function updateAttachedDoc ( existing: WithLookup | undefined, applyOp: ApplyOperations, valValue: AttachedDoc & BitrixSyncDoc ): Promise { if (existing !== undefined) { // We need to update fields if they are different. existing = await updateDoc(applyOp, existing, valValue) const existingM = hierarchy.as(existing, bitrix.mixin.BitrixSyncDoc) await updateMixin( applyOp, existingM, { type: valValue.type, bitrixId: valValue.bitrixId, rawData: valValue.rawData }, bitrix.mixin.BitrixSyncDoc ) } else { const { bitrixId, rawData, ...data } = valValue await applyOp.addCollection( valValue._class, valValue.space, valValue.attachedTo, valValue.attachedToClass, valValue.collection, data, valValue._id, valValue.modifiedOn, valValue.modifiedBy ) await applyOp.createMixin( valValue._id, valValue._class, valValue.space, bitrix.mixin.BitrixSyncDoc, { type: valValue.type, bitrixId: valValue.bitrixId, rawData: valValue.rawData }, valValue.modifiedOn, valValue.modifiedBy ) } } async function updateMainDoc (applyOp: ApplyOperations): Promise { if (existing !== undefined) { // We need update doucment id. resultDoc.document._id = existing._id as Ref // We need to update fields if they are different. return (await updateDoc(applyOp, existing, resultDoc.document)) as BitrixSyncDoc // Go over extra documents. } else { const { bitrixId, rawData, ...data } = resultDoc.document const id = await applyOp.createDoc( resultDoc.document._class, resultDoc.document.space, data, resultDoc.document._id, resultDoc.document.modifiedOn, resultDoc.document.modifiedBy ) resultDoc.document._id = id as Ref return resultDoc.document } } } async function updateMixins ( mixins: Record>, Data>, hierarchy: Hierarchy, existing: Doc, applyOp: ApplyOperations, resultDoc: BitrixSyncDoc ): Promise { for (const [m, mv] of Object.entries(mixins)) { const mRef = m as Ref> if (!hierarchy.hasMixin(existing, mRef)) { await applyOp.createMixin( resultDoc._id, resultDoc._class, resultDoc.space, m as Ref>, mv, resultDoc.modifiedOn, resultDoc.modifiedBy ) } else { const existingM = hierarchy.as(existing, mRef) await updateMixin(applyOp, existingM, mv, mRef) } } } /** * @public */ export function processComment (comment: string): string { comment = comment.replaceAll('\n', '\n
') comment = comment.replaceAll(/\[(\/?[^[\]]+)]/gi, (text: string, args: string) => { if (args.startsWith('/URL')) { return '' } if (args.startsWith('URL=')) { return `` } if (args.includes('/FONT')) { return '' } if (args.includes('FONT')) { return `` } if (args.includes('/SIZE')) { return '' } if (args.includes('SIZE')) { return `` } if (args.includes('/COLOR')) { return '' } if (args.includes('COLOR')) { return `` } if (args.includes('/IMG')) { return '"/>' } if (args.includes('IMG')) { return `