import core, { type Attribute, type Data, type Doc, type DocumentUpdate, Hierarchy, ModelDb, type Ref, type Tx, type Type } from '@hcengineering/core' import { deepEqual } from 'fast-equals' /** * @public */ export async function buildModel (existingTxes: Tx[]): Promise<{ hierarchy: Hierarchy, model: ModelDb, dropTx: Tx[] }> { existingTxes = existingTxes.filter((tx) => tx.modifiedBy === core.account.System) const dropTx: Tx[] = [] const hierarchy = new Hierarchy() const model = new ModelDb(hierarchy) // Construct existing model existingTxes.forEach(hierarchy.tx.bind(hierarchy)) for (const tx of existingTxes) { await applyTx(model, tx, dropTx) } return { hierarchy, model, dropTx } } async function applyTx (model: ModelDb, tx: Tx, dropTx: Tx[]): Promise { try { await model.tx(tx) } catch (err: any) { dropTx.push(tx) console.info('Found issue during processing of tx. Transaction', tx, 'is dropped...') } } function toUndef (value: any): any { return value === null ? undefined : value } function diffAttributes (doc: Data, newDoc: Data): DocumentUpdate { const result: DocumentUpdate = {} const allDocuments = new Map(Object.entries(doc)) const newDocuments = new Map(Object.entries(newDoc)) for (const [key, value] of allDocuments) { const newValue = toUndef(newDocuments.get(key)) if (!deepEqual(newValue, toUndef(value))) { // update is required, since values are different result[key] = newValue } } for (const [key, value] of newDocuments) { const oldValue = toUndef(allDocuments.get(key)) if (oldValue === undefined && value !== undefined) { // Update with new value. result[key] = value } } return result } /** * Generate a set of transactions to upgrade from one model to another. * @public */ export async function generateModelDiff (existingTxes: Tx[], txes: Tx[]): Promise<{ diffTx: Op[], dropTx: Tx[] }> { const { model, dropTx } = await buildModel(existingTxes) const { model: newModel } = await buildModel(txes) const diffTx = generateDocumentDiff( await model.findAll(core.class.Doc, {}), await newModel.findAll(core.class.Doc, {}) ) return { diffTx, dropTx } } export type Op = Record /** * @public */ export function generateDocumentDiff (oldDocs: Doc[], newDocs: Doc[]): Op[] { const diffTx: Op[] = [] const allDocuments = new Map(oldDocs.map((d) => [getId(d), d])) const newDocuments = new Map(newDocs.map((d) => [getId(d), d])) // Find same documents. allDocuments.forEach(handleUpdateRemove(newDocuments, diffTx)) newDocuments.forEach(handleAdd(allDocuments, diffTx)) return diffTx } function getId (d: Doc): Ref { // We need to update Attribute IDS if (d._class === core.class.Attribute) { const attr = d as Attribute> return (attr.attributeOf + '.' + attr.name) as Ref } else if (d._class === ('view:class:Viewlet' as Ref)) { const cr = d as any return ((cr.attachTo as string) + '.' + (cr.open as string)) as Ref } else if (d._class === ('workbench:class:Application' as Ref)) { const cr = d as any return ('workbench.app.' + (cr.label as string)) as Ref } else if (d._class === ('server-core:class:Trigger' as Ref)) { const cr = d as any return cr.trigger as string as Ref } return d._id } function handleAdd (allDocuments: Map, Doc>, newTxes: Op[]): (value: Doc, key: Ref) => void { return (doc, key) => { if (!allDocuments.has(key)) { // Add is required const { _id, _class, modifiedBy, modifiedOn, space, ...data } = doc const tx: Op = { _class: 'create-doc', objectId: _id, objectClass: doc._class, attributes: data } newTxes.push(tx) } } } function handleUpdateRemove (newDocuments: Map, Doc>, newTxes: Op[]): (value: Doc, key: Ref) => void { return (doc, key) => { const newDoc = newDocuments.get(key) if (newDoc !== undefined) { // update is required. const { _id, _class, modifiedBy, modifiedOn, space, ...data } = newDoc const { _id: _0, _class: _1, modifiedBy: _2, modifiedOn: _3, space: _4, ...oldData } = doc const operations = diffAttributes(oldData, data) if (Object.keys(operations).length > 0) { const tx: Op = { _class: 'update-doc', objectId: _id, objectClass: _class, operations } newTxes.push(tx) } } else { // Delete is required const { _id: oldId, _class: _1, modifiedBy: _2, modifiedOn: _3, space: _4, ...oldData } = doc const tx: Op = { _class: 'remove-doc', objectId: oldId, objectClass: doc._class, data: oldData } newTxes.push(tx) } } } export function printDiff (diffTx: Op[]): void { // Collect Classes. console.log('Diff Transactions', JSON.stringify(diffTx, undefined, 2)) }