2023-12-13 15:59:11 +00:00
|
|
|
import {
|
2024-02-05 14:35:49 +00:00
|
|
|
Account,
|
2023-12-13 15:59:11 +00:00
|
|
|
AttachedDoc,
|
|
|
|
Class,
|
|
|
|
Doc,
|
|
|
|
Hierarchy,
|
|
|
|
Mixin,
|
|
|
|
Ref,
|
|
|
|
RefTo,
|
|
|
|
TxCollectionCUD,
|
|
|
|
TxCreateDoc,
|
|
|
|
TxCUD,
|
|
|
|
TxMixin,
|
|
|
|
TxProcessor,
|
|
|
|
TxUpdateDoc
|
|
|
|
} from '@hcengineering/core'
|
|
|
|
import core from '@hcengineering/core/lib/component'
|
|
|
|
import { DocAttributeUpdates, DocUpdateAction } from '@hcengineering/activity'
|
|
|
|
import { ActivityControl, DocObjectCache, getAllObjectTransactions } from '@hcengineering/server-activity'
|
2024-02-05 14:35:49 +00:00
|
|
|
import { getDocCollaborators } from '@hcengineering/server-notification-resources'
|
|
|
|
import notification from '@hcengineering/notification'
|
|
|
|
import { TriggerControl } from '@hcengineering/server-core'
|
2023-12-13 15:59:11 +00:00
|
|
|
|
|
|
|
function getAvailableAttributesKeys (tx: TxCUD<Doc>, hierarchy: Hierarchy): string[] {
|
|
|
|
if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) {
|
|
|
|
const updateTx = tx as TxUpdateDoc<Doc>
|
|
|
|
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<Doc, Doc>
|
|
|
|
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<Doc>, hierarchy: Hierarchy): Record<string, any> {
|
|
|
|
if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) {
|
|
|
|
const updateTx = tx as TxUpdateDoc<Doc>
|
|
|
|
|
|
|
|
return updateTx.operations as Record<string, any>
|
|
|
|
}
|
|
|
|
if (hierarchy.isDerived(tx._class, core.class.TxMixin)) {
|
|
|
|
const mixinTx = tx as TxMixin<Doc, Doc>
|
|
|
|
return mixinTx.attributes as Record<string, any>
|
|
|
|
}
|
|
|
|
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<Doc>): 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<Class<Doc>>,
|
|
|
|
objectId: Ref<Doc>,
|
|
|
|
lastTxId: Ref<TxCUD<Doc>>,
|
|
|
|
mixin?: Ref<Mixin<Doc>>,
|
|
|
|
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<Doc, AttachedDoc>).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<Doc>)
|
|
|
|
|
|
|
|
for (const objectTx of objectTxes) {
|
|
|
|
const actualTx = TxProcessor.extractTx(objectTx) as TxCUD<Doc>
|
|
|
|
|
|
|
|
if (actualTx._class === core.class.TxUpdateDoc) {
|
|
|
|
prevDoc = hierarchy.clone(doc)
|
|
|
|
doc = TxProcessor.updateDoc2Doc(doc, actualTx as TxUpdateDoc<Doc>)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actualTx._class === core.class.TxMixin) {
|
|
|
|
prevDoc = hierarchy.clone(doc)
|
|
|
|
doc = TxProcessor.updateMixin4Doc(doc, actualTx as TxMixin<Doc, Doc>)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (objectTx._id === lastTxId) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { doc, prevDoc }
|
|
|
|
}
|
|
|
|
|
2024-02-05 14:35:49 +00:00
|
|
|
interface AttributeDiff {
|
|
|
|
added: DocAttributeUpdates['added']
|
|
|
|
removed: DocAttributeUpdates['removed']
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getCollaboratorsDiff (
|
|
|
|
control: ActivityControl,
|
|
|
|
doc: Doc,
|
|
|
|
prevDoc: Doc | undefined
|
|
|
|
): Promise<AttributeDiff> {
|
|
|
|
const { hierarchy } = control
|
|
|
|
const value = hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? []
|
|
|
|
|
|
|
|
let prevValue: Ref<Account>[] = []
|
|
|
|
|
|
|
|
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,
|
2023-12-13 15:59:11 +00:00
|
|
|
doc: Doc,
|
|
|
|
prevDoc: Doc | undefined,
|
|
|
|
attrKey: string,
|
|
|
|
attrClass: Ref<Class<Doc>>,
|
|
|
|
isMixin: boolean
|
2024-02-05 14:35:49 +00:00
|
|
|
): Promise<AttributeDiff> {
|
|
|
|
const { hierarchy } = control
|
|
|
|
|
2023-12-13 15:59:11 +00:00
|
|
|
let actualDoc: Doc | undefined = doc
|
|
|
|
let actualPrevDoc: Doc | undefined = prevDoc
|
|
|
|
|
2024-02-05 14:35:49 +00:00
|
|
|
if (isMixin && hierarchy.isDerived(attrClass, notification.mixin.Collaborators)) {
|
|
|
|
return await getCollaboratorsDiff(control, doc, prevDoc)
|
|
|
|
}
|
|
|
|
|
2023-12-13 15:59:11 +00:00
|
|
|
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<Doc>,
|
|
|
|
tx: TxCUD<Doc>,
|
|
|
|
object: Doc,
|
|
|
|
objectCache?: DocObjectCache
|
|
|
|
): Promise<DocAttributeUpdates[]> {
|
|
|
|
if (![core.class.TxMixin, core.class.TxUpdateDoc].includes(tx._class)) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
2024-01-29 11:54:21 +00:00
|
|
|
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 []
|
|
|
|
}
|
|
|
|
|
2023-12-13 15:59:11 +00:00
|
|
|
const hierarchy = control.hierarchy
|
|
|
|
const keys = getAvailableAttributesKeys(tx, hierarchy)
|
|
|
|
|
|
|
|
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<Doc, Doc>).mixin : undefined
|
|
|
|
|
2024-01-29 11:54:21 +00:00
|
|
|
const { doc, prevDoc } = await getDocDiff(
|
|
|
|
control,
|
|
|
|
updateObject._class,
|
|
|
|
updateObject._id,
|
|
|
|
originTx._id,
|
|
|
|
mixin,
|
|
|
|
objectCache
|
|
|
|
)
|
2023-12-13 15:59:11 +00:00
|
|
|
|
|
|
|
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<Class<Doc>> | undefined = mixin
|
|
|
|
|
2024-01-29 11:54:21 +00:00
|
|
|
const clazz = hierarchy.findAttribute(updateObject._class, key)
|
2023-12-13 15:59:11 +00:00
|
|
|
|
|
|
|
if (clazz !== undefined && 'to' in clazz.type) {
|
|
|
|
attrClass = clazz.type.to as Ref<Class<Doc>>
|
|
|
|
} else if (clazz !== undefined && 'of' in clazz?.type) {
|
|
|
|
attrClass = (clazz.type.of as RefTo<Doc>).to
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attrClass == null && clazz?.type?._class !== undefined) {
|
|
|
|
attrClass = clazz.type._class
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attrClass === undefined) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Array.isArray(attrValue) && doc != null) {
|
2024-02-05 14:35:49 +00:00
|
|
|
const diff = await getAttributeDiff(control, doc, prevDoc, key, attrClass, isMixin)
|
2023-12-13 15:59:11 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-29 11:54:21 +00:00
|
|
|
let setAttr = []
|
|
|
|
|
|
|
|
if (Array.isArray(attrValue)) {
|
|
|
|
setAttr = attrValue
|
|
|
|
} else if (key in modifiedAttributes) {
|
|
|
|
setAttr = [attrValue]
|
|
|
|
}
|
|
|
|
|
2023-12-13 15:59:11 +00:00
|
|
|
result.push({
|
|
|
|
attrKey: key,
|
|
|
|
attrClass,
|
2024-01-29 11:54:21 +00:00
|
|
|
set: setAttr,
|
2023-12-13 15:59:11 +00:00
|
|
|
added,
|
|
|
|
removed,
|
|
|
|
prevValue,
|
|
|
|
isMixin
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
function getHiddenAttrs (hierarchy: Hierarchy, _class: Ref<Class<Doc>>): Set<string> {
|
|
|
|
return new Set(
|
|
|
|
[...hierarchy.getAllAttributes(_class).entries()].filter(([, attr]) => attr.hidden === true).map(([k]) => k)
|
|
|
|
)
|
|
|
|
}
|