forked from tsai/budibase
13 changed files with 517 additions and 21 deletions
@ -0,0 +1,167 @@ |
|||||
|
import { getFlattenedHierarchy, isRecord, isIndex, isAncestor } from "./hierarchy" |
||||
|
import { $, none } from "../common" |
||||
|
import { map, filter, some, find } from "lodash/fp" |
||||
|
|
||||
|
export const HierarchyChangeTypes = { |
||||
|
recordCreated: "Record Created", |
||||
|
recordDeleted: "Record Deleted", |
||||
|
recordRenamed: "Record Renamed", |
||||
|
recordFieldsChanged: "Record Fields Changed", |
||||
|
recordEstimatedRecordTypeChanged: "Record's Estimated Record Count Changed", |
||||
|
indexCreated: "Index Created", |
||||
|
indexDeleted: "Index Deleted", |
||||
|
indexChanged: "index Changed", |
||||
|
} |
||||
|
|
||||
|
export const diffHierarchy = (oldHierarchy, newHierarchy) => { |
||||
|
const oldHierarchyFlat = getFlattenedHierarchy(oldHierarchy) |
||||
|
const newHierarchyFlat = getFlattenedHierarchy(newHierarchy) |
||||
|
|
||||
|
const createdRecords = findCreatedRecords(oldHierarchyFlat, newHierarchyFlat) |
||||
|
const deletedRecords = findDeletedRecords(oldHierarchyFlat, newHierarchyFlat) |
||||
|
|
||||
|
return [ |
||||
|
...createdRecords, |
||||
|
...deletedRecords, |
||||
|
...findRenamedRecords(oldHierarchyFlat, newHierarchyFlat), |
||||
|
...findRecordsWithFieldsChanged(oldHierarchyFlat, newHierarchyFlat), |
||||
|
...findRecordsWithEstimatedRecordTypeChanged(oldHierarchyFlat, newHierarchyFlat), |
||||
|
...findCreatedIndexes(oldHierarchyFlat, newHierarchyFlat, createdRecords), |
||||
|
...findDeletedIndexes(oldHierarchyFlat, newHierarchyFlat, deletedRecords), |
||||
|
...findUpdatedIndexes(oldHierarchyFlat, newHierarchyFlat), |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
const changeItem = (type, oldNode, newNode) => ({ |
||||
|
type, oldNode, newNode, |
||||
|
}) |
||||
|
|
||||
|
const findCreatedRecords = (oldHierarchyFlat, newHierarchyFlat) => { |
||||
|
const allCreated = $(newHierarchyFlat, [ |
||||
|
filter(isRecord), |
||||
|
filter(nodeDoesNotExistIn(oldHierarchyFlat)), |
||||
|
map(n => changeItem(HierarchyChangeTypes.recordCreated, null, n)) |
||||
|
]) |
||||
|
|
||||
|
return $(allCreated, [ |
||||
|
filter(r => none(r2 => isAncestor(r.newNode)(r2.newNode))(allCreated)) |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
const findDeletedRecords = (oldHierarchyFlat, newHierarchyFlat) => { |
||||
|
const allDeleted = $(oldHierarchyFlat, [ |
||||
|
filter(isRecord), |
||||
|
filter(nodeDoesNotExistIn(newHierarchyFlat)), |
||||
|
map(n => changeItem(HierarchyChangeTypes.recordDeleted, n, null)) |
||||
|
]) |
||||
|
|
||||
|
return $(allDeleted, [ |
||||
|
filter(r => none(r2 => isAncestor(r.oldNode)(r2.oldNode))(allDeleted)) |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
const findRenamedRecords = (oldHierarchyFlat, newHierarchyFlat) => |
||||
|
$(oldHierarchyFlat, [ |
||||
|
filter(isRecord), |
||||
|
filter(nodeExistsIn(newHierarchyFlat)), |
||||
|
filter(nodeChanged(newHierarchyFlat, (_new,old) =>_new.collectionKey !== old.collectionKey )), |
||||
|
map(n => changeItem( |
||||
|
HierarchyChangeTypes.recordRenamed, |
||||
|
n, |
||||
|
findNodeIn(n, newHierarchyFlat)) |
||||
|
) |
||||
|
]) |
||||
|
|
||||
|
const findRecordsWithFieldsChanged = (oldHierarchyFlat, newHierarchyFlat) => |
||||
|
$(oldHierarchyFlat, [ |
||||
|
filter(isRecord), |
||||
|
filter(nodeExistsIn(newHierarchyFlat)), |
||||
|
filter(hasDifferentFields(newHierarchyFlat)), |
||||
|
map(n => changeItem( |
||||
|
HierarchyChangeTypes.recordFieldsChanged, |
||||
|
n, |
||||
|
findNodeIn(n, newHierarchyFlat)) |
||||
|
) |
||||
|
]) |
||||
|
|
||||
|
const findRecordsWithEstimatedRecordTypeChanged = (oldHierarchyFlat, newHierarchyFlat) => |
||||
|
$(oldHierarchyFlat, [ |
||||
|
filter(isRecord), |
||||
|
filter(nodeExistsIn(newHierarchyFlat)), |
||||
|
filter(nodeChanged(newHierarchyFlat, (_new,old) =>_new.estimatedRecordCount !== old.estimatedRecordCount)), |
||||
|
map(n => changeItem( |
||||
|
HierarchyChangeTypes.recordEstimatedRecordTypeChanged, |
||||
|
n, |
||||
|
findNodeIn(n, newHierarchyFlat)) |
||||
|
) |
||||
|
]) |
||||
|
|
||||
|
const findCreatedIndexes = (oldHierarchyFlat, newHierarchyFlat, createdRecords) => { |
||||
|
const allCreated = $(newHierarchyFlat, [ |
||||
|
filter(isIndex), |
||||
|
filter(nodeDoesNotExistIn(oldHierarchyFlat)), |
||||
|
map(n => changeItem(HierarchyChangeTypes.indexCreated, null, n)) |
||||
|
]) |
||||
|
|
||||
|
return $(allCreated, [ |
||||
|
filter(r => none(r2 => isAncestor(r.newNode)(r2.newNode))(createdRecords)) |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
const findDeletedIndexes = (oldHierarchyFlat, newHierarchyFlat, deletedRecords) => { |
||||
|
const allDeleted = $(oldHierarchyFlat, [ |
||||
|
filter(isIndex), |
||||
|
filter(nodeDoesNotExistIn(newHierarchyFlat)), |
||||
|
map(n => changeItem(HierarchyChangeTypes.indexDeleted, n, null)) |
||||
|
]) |
||||
|
|
||||
|
return $(allDeleted, [ |
||||
|
filter(r => none(r2 => isAncestor(r.oldNode)(r2.oldNode))(deletedRecords)) |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const findUpdatedIndexes = (oldHierarchyFlat, newHierarchyFlat) => |
||||
|
$(oldHierarchyFlat, [ |
||||
|
filter(isRecord), |
||||
|
filter(nodeExistsIn(newHierarchyFlat)), |
||||
|
filter(nodeChanged(newHierarchyFlat, indexHasChanged)), |
||||
|
map(n => changeItem( |
||||
|
HierarchyChangeTypes.indexChanged, |
||||
|
n, |
||||
|
findNodeIn(n, newHierarchyFlat)) |
||||
|
) |
||||
|
]) |
||||
|
|
||||
|
const hasDifferentFields = otherFlatHierarchy => record1 => { |
||||
|
|
||||
|
const record2 = findNodeIn(record1, otherFlatHierarchy) |
||||
|
|
||||
|
if(record1.fields.length !== record2.fields.length) return true |
||||
|
|
||||
|
for(let f1 of record1.fields) { |
||||
|
if (none(isFieldSame(f1))(record2.fields)) return true |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
const indexHasChanged = (_new, old) => |
||||
|
_new.map !== old.map |
||||
|
|| _new.filter !== old.filter |
||||
|
|| _new.getShardName !== old.getShardName |
||||
|
|
||||
|
const isFieldSame = f1 => f2 => |
||||
|
f1.name === f2.name && f1.type === f2.type |
||||
|
|
||||
|
const nodeDoesNotExistIn = inThis => node => |
||||
|
none(n => n.nodeId === node.nodeId)(inThis) |
||||
|
|
||||
|
const nodeExistsIn = inThis => node => |
||||
|
some(n => n.nodeId === node.nodeId)(inThis) |
||||
|
|
||||
|
const nodeChanged = (inThis, isChanged) => node => |
||||
|
some(n => n.nodeId === node.nodeId && isChanged(n, node))(inThis) |
||||
|
|
||||
|
const findNodeIn = (node, inThis) => |
||||
|
find(n => n.nodeId === node.nodeId)(inThis) |
||||
@ -0,0 +1,17 @@ |
|||||
|
/* |
||||
|
const changeActions = { |
||||
|
rebuildIndex: indexNodeKey => ({ |
||||
|
type: "rebuildIndex", |
||||
|
indexNodeKey, |
||||
|
}), |
||||
|
reshardRecords: recordNodeKey => ({ |
||||
|
type: "reshardRecords", |
||||
|
recordNodeKey, |
||||
|
}), |
||||
|
deleteRecords: recordNodeKey => ({ |
||||
|
type: "reshardRecords", |
||||
|
recordNodeKey, |
||||
|
}), |
||||
|
renameRecord |
||||
|
} |
||||
|
*/ |
||||
@ -0,0 +1,217 @@ |
|||||
|
import { getMemoryTemplateApi } from "./specHelpers" |
||||
|
import { diffHierarchy, HierarchyChangeTypes } from "../src/templateApi/diffHierarchy" |
||||
|
import { getFlattenedHierarchy } from "../src/templateApi/hierarchy" |
||||
|
|
||||
|
describe("diffHierarchy", () => { |
||||
|
|
||||
|
it("should not show any changes, when hierarchy is unchanged", async () => { |
||||
|
const oldHierarchy = (await setup()).root; |
||||
|
const newHierarchy = (await setup()).root; |
||||
|
const diff = diffHierarchy(oldHierarchy, newHierarchy) |
||||
|
expect(diff).toEqual([]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect root record created", async () => { |
||||
|
const oldHierarchy = (await setup()).root; |
||||
|
const newSetup = (await setup()); |
||||
|
const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.root, "opportunity", false) |
||||
|
const diff = diffHierarchy(oldHierarchy, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: opportunity, |
||||
|
oldNode: null, |
||||
|
type: HierarchyChangeTypes.recordCreated |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should only detect root record, when newly created root record has children ", async () => { |
||||
|
const oldHierarchy = (await setup()).root; |
||||
|
const newSetup = (await setup()); |
||||
|
const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.root, "opportunity", false) |
||||
|
newSetup.templateApi.getNewRecordTemplate(opportunity, "invoice", true) |
||||
|
const diff = diffHierarchy(oldHierarchy, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: opportunity, |
||||
|
oldNode: null, |
||||
|
type: HierarchyChangeTypes.recordCreated |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect child record created", async () => { |
||||
|
const oldHierarchy = (await setup()).root; |
||||
|
const newSetup = (await setup()); |
||||
|
const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.contact, "opportunity", false) |
||||
|
const diff = diffHierarchy(oldHierarchy, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: opportunity, |
||||
|
oldNode: null, |
||||
|
type: HierarchyChangeTypes.recordCreated |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect root record deleted", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
newSetup.root.children = newSetup.root.children.filter(n => n.name !== "contact") |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: null, |
||||
|
oldNode: oldSetup.contact, |
||||
|
type: HierarchyChangeTypes.recordDeleted |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect child record deleted", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
newSetup.contact.children = newSetup.contact.children.filter(n => n.name !== "deal") |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: null, |
||||
|
oldNode: oldSetup.deal, |
||||
|
type: HierarchyChangeTypes.recordDeleted |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect root record renamed", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
newSetup.contact.collectionKey = "CONTACTS" |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: newSetup.contact, |
||||
|
oldNode: oldSetup.contact, |
||||
|
type: HierarchyChangeTypes.recordRenamed |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect child record renamed", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
newSetup.deal.collectionKey = "CONTACTS" |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: newSetup.deal, |
||||
|
oldNode: oldSetup.deal, |
||||
|
type: HierarchyChangeTypes.recordRenamed |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect root record field removed", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
newSetup.contact.fields = newSetup.contact.fields.filter(f => f.name !== "name") |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: newSetup.contact, |
||||
|
oldNode: oldSetup.contact, |
||||
|
type: HierarchyChangeTypes.recordFieldsChanged |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect child record field removed", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
newSetup.deal.fields = newSetup.deal.fields.filter(f => f.name !== "name") |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: newSetup.deal, |
||||
|
oldNode: oldSetup.deal, |
||||
|
type: HierarchyChangeTypes.recordFieldsChanged |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect record field added", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
const notesField = newSetup.templateApi.getNewField("string") |
||||
|
notesField.name = "notes" |
||||
|
newSetup.templateApi.addField(newSetup.contact, notesField) |
||||
|
|
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: newSetup.contact, |
||||
|
oldNode: oldSetup.contact, |
||||
|
type: HierarchyChangeTypes.recordFieldsChanged |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect 1 record field added and 1 removed (total no. fields unchanged)", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
const notesField = newSetup.templateApi.getNewField("string") |
||||
|
notesField.name = "notes" |
||||
|
newSetup.templateApi.addField(newSetup.contact, notesField) |
||||
|
newSetup.contact.fields = newSetup.contact.fields.filter(f => f.name !== "name") |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: newSetup.contact, |
||||
|
oldNode: oldSetup.contact, |
||||
|
type: HierarchyChangeTypes.recordFieldsChanged |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect root record estimated record count changed", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
newSetup.contact.estimatedRecordCount = 987 |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: newSetup.contact, |
||||
|
oldNode: oldSetup.contact, |
||||
|
type: HierarchyChangeTypes.recordEstimatedRecordTypeChanged |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect root record estimated record count changed", async () => { |
||||
|
const oldSetup = (await setup()); |
||||
|
const newSetup = (await setup()); |
||||
|
newSetup.deal.estimatedRecordCount = 987 |
||||
|
const diff = diffHierarchy(oldSetup.root, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: newSetup.deal, |
||||
|
oldNode: oldSetup.deal, |
||||
|
type: HierarchyChangeTypes.recordEstimatedRecordTypeChanged |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
it("should detect root record created", async () => { |
||||
|
const oldHierarchy = (await setup()).root; |
||||
|
const newSetup = (await setup()); |
||||
|
const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.root, "opportunity", false) |
||||
|
const diff = diffHierarchy(oldHierarchy, newSetup.root) |
||||
|
expect(diff).toEqual([{ |
||||
|
newNode: opportunity, |
||||
|
oldNode: null, |
||||
|
type: HierarchyChangeTypes.recordCreated |
||||
|
}]) |
||||
|
}) |
||||
|
|
||||
|
}) |
||||
|
|
||||
|
const setup = async () => { |
||||
|
const { templateApi } = await getMemoryTemplateApi() |
||||
|
const root = templateApi.getNewRootLevel() |
||||
|
const contact = templateApi.getNewRecordTemplate(root, "contact", true) |
||||
|
|
||||
|
const nameField = templateApi.getNewField("string") |
||||
|
nameField.name = "name" |
||||
|
const statusField = templateApi.getNewField("string") |
||||
|
statusField.name = "status" |
||||
|
|
||||
|
templateApi.addField(contact, nameField) |
||||
|
templateApi.addField(contact, statusField) |
||||
|
|
||||
|
const lead = templateApi.getNewRecordTemplate(root, "lead", true) |
||||
|
const deal = templateApi.getNewRecordTemplate(contact, "deal", true) |
||||
|
|
||||
|
templateApi.addField(deal, {...nameField}) |
||||
|
templateApi.addField(deal, {...statusField}) |
||||
|
|
||||
|
getFlattenedHierarchy(root) |
||||
|
return { |
||||
|
root, contact, lead, deal, templateApi, |
||||
|
all_contacts: root.indexes[0], |
||||
|
all_leads: root.indexes[1], |
||||
|
deals_for_contacts: contact.indexes[0] |
||||
|
} |
||||
|
} |
||||
@ -1,7 +1,15 @@ |
|||||
exports.getRecordKey = (appname, wholePath) => |
exports.getRecordKey = (appname, wholePath) => |
||||
wholePath |
this.getAppRelativePath(appname, wholePath) |
||||
.replace(`/${appname}/api/files/`, "") |
.replace(`/api/files/`, "/") |
||||
.replace(`/${appname}/api/lookup_field/`, "") |
.replace(`/api/lookup_field/`, "/") |
||||
.replace(`/${appname}/api/record/`, "") |
.replace(`/api/record/`, "/") |
||||
.replace(`/${appname}/api/listRecords/`, "") |
.replace(`/api/listRecords/`, "/") |
||||
.replace(`/${appname}/api/aggregates/`, "") |
.replace(`/api/aggregates/`, "/") |
||||
|
|
||||
|
exports.getAppRelativePath = (appname, wholePath) => { |
||||
|
const builderInstanceRegex = new RegExp( |
||||
|
`\\/_builder\\/instance\\/[^\\/]*\\/[^\\/]*\\/` |
||||
|
) |
||||
|
|
||||
|
return wholePath.replace(builderInstanceRegex, "/").replace(`/${appname}`, "") |
||||
|
} |
||||
|
|||||
Loading…
Reference in new issue