platform/dev/tool/src/mdiff.ts
Andrey Sobolev c23af625f6
Enhanced Phased build (#4599)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
2024-02-12 23:57:11 +07:00

165 lines
4.9 KiB
TypeScript

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<void> {
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<Doc>, newDoc: Data<Doc>): DocumentUpdate<Doc> {
const result: DocumentUpdate<any> = {}
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<string, any>
/**
* @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<Doc> {
// We need to update Attribute IDS
if (d._class === core.class.Attribute) {
const attr = d as Attribute<Type<any>>
return (attr.attributeOf + '.' + attr.name) as Ref<Doc>
} else if (d._class === ('view:class:Viewlet' as Ref<Doc>)) {
const cr = d as any
return ((cr.attachTo as string) + '.' + (cr.open as string)) as Ref<Doc>
} else if (d._class === ('workbench:class:Application' as Ref<Doc>)) {
const cr = d as any
return ('workbench.app.' + (cr.label as string)) as Ref<Doc>
} else if (d._class === ('server-core:class:Trigger' as Ref<Doc>)) {
const cr = d as any
return cr.trigger as string as Ref<Doc>
}
return d._id
}
function handleAdd (allDocuments: Map<Ref<Doc>, Doc>, newTxes: Op[]): (value: Doc, key: Ref<Doc>) => 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<Ref<Doc>, Doc>, newTxes: Op[]): (value: Doc, key: Ref<Doc>) => 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))
}