import { Account, AttachedDoc, Class, Doc, Hierarchy, Mixin, Ref, RefTo, TxCollectionCUD, TxCreateDoc, TxCUD, TxMixin, TxProcessor, TxUpdateDoc } from '@hcengineering/core' import core from '@hcengineering/core/lib/component' import { ActivityMessageControl, DocAttributeUpdates, DocUpdateAction } from '@hcengineering/activity' import { ActivityControl, DocObjectCache, getAllObjectTransactions } from '@hcengineering/server-activity' import { getDocCollaborators } from '@hcengineering/server-notification-resources' import notification from '@hcengineering/notification' import { TriggerControl } from '@hcengineering/server-core' function getAvailableAttributesKeys (tx: TxCUD, hierarchy: Hierarchy): string[] { if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { const updateTx = tx as TxUpdateDoc const _class = updateTx.objectClass try { hierarchy.getClass(_class) } catch (err: any) { // class is deleted return [] } const hiddenAttrs = getHiddenAttrs(hierarchy, _class) return Object.entries(updateTx.operations) .flatMap(([id, val]) => (['$push', '$pull'].includes(id) ? Object.keys(val) : id)) .filter((id) => !id.startsWith('$') && !hiddenAttrs.has(id)) } if (hierarchy.isDerived(tx._class, core.class.TxMixin)) { const mixinTx = tx as TxMixin const _class = mixinTx.mixin try { hierarchy.getClass(_class) } catch (err: any) { // mixin is deleted return [] } const hiddenAttrs = getHiddenAttrs(hierarchy, _class) return Object.keys(mixinTx.attributes) .filter((id) => !id.startsWith('$')) .filter((key) => !hiddenAttrs.has(key)) } return [] } function getModifiedAttributes (tx: TxCUD, hierarchy: Hierarchy): Record { if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { const updateTx = tx as TxUpdateDoc return updateTx.operations as Record } if (hierarchy.isDerived(tx._class, core.class.TxMixin)) { const mixinTx = tx as TxMixin return mixinTx.attributes as Record } return {} } function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] { return Array.from( new Set( attributes.flatMap((attr) => Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key] ) ) ).filter((v) => v != null) } export function getDocUpdateAction (control: ActivityControl, tx: TxCUD): DocUpdateAction { const hierarchy = control.hierarchy if (hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) { return 'create' } if (hierarchy.isDerived(tx._class, core.class.TxRemoveDoc)) { return 'remove' } return 'update' } export async function getDocDiff ( control: ActivityControl, _class: Ref>, objectId: Ref, lastTxId: Ref>, mixin?: Ref>, objectCache?: DocObjectCache ): Promise<{ doc?: Doc, prevDoc?: Doc }> { const hierarchy = control.hierarchy const isAttached = hierarchy.isDerived(_class, core.class.AttachedDoc) const objectTxes = objectCache?.transactions.get(objectId) ?? (await getAllObjectTransactions(control, _class, [objectId], mixin)).get(objectId) ?? [] const createTx = isAttached ? objectTxes.find((tx) => (tx as TxCollectionCUD).tx?._class === core.class.TxCreateDoc) : objectTxes.find((tx) => tx._class === core.class.TxCreateDoc) if (createTx === undefined) { return {} } let doc: Doc | undefined let prevDoc: Doc | undefined doc = TxProcessor.createDoc2Doc(TxProcessor.extractTx(createTx) as TxCreateDoc) for (const objectTx of objectTxes) { const actualTx = TxProcessor.extractTx(objectTx) as TxCUD if (actualTx._class === core.class.TxUpdateDoc) { prevDoc = hierarchy.clone(doc) doc = TxProcessor.updateDoc2Doc(doc, actualTx as TxUpdateDoc) } if (actualTx._class === core.class.TxMixin) { prevDoc = hierarchy.clone(doc) doc = TxProcessor.updateMixin4Doc(doc, actualTx as TxMixin) } if (objectTx._id === lastTxId) { break } } return { doc, prevDoc } } interface AttributeDiff { added: DocAttributeUpdates['added'] removed: DocAttributeUpdates['removed'] } async function getCollaboratorsDiff ( control: ActivityControl, doc: Doc, prevDoc: Doc | undefined ): Promise { const { hierarchy } = control const value = hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] let prevValue: Ref[] = [] if (prevDoc !== undefined && hierarchy.hasMixin(prevDoc, notification.mixin.Collaborators)) { prevValue = hierarchy.as(prevDoc, notification.mixin.Collaborators).collaborators ?? [] } else if (prevDoc !== undefined) { const mixin = hierarchy.classHierarchyMixin(prevDoc._class, notification.mixin.ClassCollaborators) prevValue = mixin !== undefined ? await getDocCollaborators(prevDoc, mixin, control as TriggerControl) : [] } const added = value.filter((item) => !prevValue.includes(item)) as DocAttributeUpdates['added'] const removed = prevValue.filter((item) => !value.includes(item)) as DocAttributeUpdates['removed'] return { added, removed } } export async function getAttributeDiff ( control: ActivityControl, doc: Doc, prevDoc: Doc | undefined, attrKey: string, attrClass: Ref>, isMixin: boolean ): Promise { const { hierarchy } = control let actualDoc: Doc | undefined = doc let actualPrevDoc: Doc | undefined = prevDoc if (isMixin && hierarchy.isDerived(attrClass, notification.mixin.Collaborators)) { return await getCollaboratorsDiff(control, doc, prevDoc) } if (isMixin) { actualDoc = hierarchy.as(doc, attrClass) actualPrevDoc = prevDoc === undefined ? undefined : hierarchy.as(prevDoc, attrClass) } const value = (actualDoc as any)[attrKey] ?? [] const prevValue = (actualPrevDoc as any)?.[attrKey] ?? [] if (!Array.isArray(value) || !Array.isArray(prevValue)) { return { added: [], removed: [] } } const added = value.filter((item) => !prevValue.includes(item)) as DocAttributeUpdates['added'] const removed = prevValue.filter((item) => !value.includes(item)) as DocAttributeUpdates['removed'] return { added, removed } } export async function getTxAttributesUpdates ( control: ActivityControl, originTx: TxCUD, tx: TxCUD, object: Doc, objectCache?: DocObjectCache, controlRules?: ActivityMessageControl[] ): Promise { if (![core.class.TxMixin, core.class.TxUpdateDoc].includes(tx._class)) { return [] } let updateObject = object if (updateObject._id !== tx.objectId) { updateObject = objectCache?.docs?.get(tx.objectId) ?? (await control.findAll(tx.objectClass, { _id: tx.objectId }))[0] } if (updateObject === undefined) { return [] } const hierarchy = control.hierarchy const filterSet = new Set() for (const c of controlRules ?? []) { for (const f of c.skipFields ?? []) { filterSet.add(f) } } const keys = getAvailableAttributesKeys(tx, hierarchy).filter((it) => !filterSet.has(it)) if (keys.length === 0) { return [] } const result: DocAttributeUpdates[] = [] const modifiedAttributes = getModifiedAttributes(tx, hierarchy) const isMixin = hierarchy.isDerived(tx._class, core.class.TxMixin) const mixin = isMixin ? (tx as TxMixin).mixin : undefined const { doc, prevDoc } = await getDocDiff( control, updateObject._class, updateObject._id, originTx._id, mixin, objectCache ) for (const key of keys) { let attrValue = modifiedAttributes[key] let prevValue const added = combineAttributes([modifiedAttributes], key, '$push', '$each') const removed = combineAttributes([modifiedAttributes], key, '$pull', '$in') let attrClass: Ref> | undefined = mixin const clazz = hierarchy.findAttribute(updateObject._class, key) if (clazz !== undefined && 'to' in clazz.type) { attrClass = clazz.type.to as Ref> } else if (clazz !== undefined && 'of' in clazz?.type) { attrClass = (clazz.type.of as RefTo).to } if (attrClass == null && clazz?.type?._class !== undefined) { attrClass = clazz.type._class } if (attrClass === undefined) { continue } if (Array.isArray(attrValue) && doc != null) { const diff = await getAttributeDiff(control, doc, prevDoc, key, attrClass, isMixin) added.push(...diff.added) removed.push(...diff.removed) attrValue = [] } if (prevDoc !== undefined) { const rawPrevValue = isMixin ? (hierarchy.as(prevDoc, attrClass) as any)[key] : (prevDoc as any)[key] if (Array.isArray(rawPrevValue)) { prevValue = rawPrevValue } else if (rawPrevValue !== undefined && rawPrevValue !== null && typeof rawPrevValue === 'object') { prevValue = rawPrevValue._id } else { prevValue = rawPrevValue } } let setAttr = [] if (Array.isArray(attrValue)) { setAttr = attrValue } else if (key in modifiedAttributes) { setAttr = [attrValue] } result.push({ attrKey: key, attrClass, set: setAttr, added, removed, prevValue, isMixin }) } return result } function getHiddenAttrs (hierarchy: Hierarchy, _class: Ref>): Set { return new Set( [...hierarchy.getAllAttributes(_class).entries()].filter(([, attr]) => attr.hidden === true).map(([k]) => k) ) }