mirror of https://github.com/Budibase/budibase.git
54 changed files with 1317 additions and 360 deletions
@ -0,0 +1,8 @@ |
|||||
|
import { setCleanupFunc } from "../transactions/setCleanupFunc" |
||||
|
|
||||
|
export const cloneApp = (app, mergeWith) => { |
||||
|
const newApp = { ...app } |
||||
|
Object.assign(newApp, mergeWith) |
||||
|
setCleanupFunc(newApp) |
||||
|
return newApp |
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
import { isString, flatten, map, filter } from "lodash/fp" |
||||
|
import { initialiseChildCollections } from "../collectionApi/initialise" |
||||
|
import { _loadFromInfo } from "./load" |
||||
|
import { $ } from "../common" |
||||
|
import { |
||||
|
getFlattenedHierarchy, |
||||
|
isRecord, |
||||
|
getNode, |
||||
|
isTopLevelRecord, |
||||
|
fieldReversesReferenceToNode, |
||||
|
} from "../templateApi/hierarchy" |
||||
|
import { initialiseIndex } from "../indexing/initialiseIndex" |
||||
|
import { getRecordInfo } from "./recordInfo" |
||||
|
|
||||
|
export const initialiseChildren = async (app, recordInfoOrKey) => { |
||||
|
const recordInfo = isString(recordInfoOrKey) |
||||
|
? getRecordInfo(app.hierarchy, recordInfoOrKey) |
||||
|
: recordInfoOrKey |
||||
|
await initialiseReverseReferenceIndexes(app, recordInfo) |
||||
|
await initialiseAncestorIndexes(app, recordInfo) |
||||
|
await initialiseChildCollections(app, recordInfo) |
||||
|
} |
||||
|
|
||||
|
export const initialiseChildrenForNode = async (app, recordNode) => { |
||||
|
|
||||
|
if (isTopLevelRecord(recordNode)) { |
||||
|
await initialiseChildren( |
||||
|
app, recordNode.nodeKey()) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const iterate = await getAllIdsIterator(app)(recordNode.parent().nodeKey()) |
||||
|
let iterateResult = await iterate() |
||||
|
while (!iterateResult.done) { |
||||
|
const { result } = iterateResult |
||||
|
for (const id of result.ids) { |
||||
|
const initialisingRecordKey = joinKey( |
||||
|
result.collectionKey, id) |
||||
|
await initialiseChildren(app, initialisingRecordKey) |
||||
|
} |
||||
|
iterateResult = await iterate() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const initialiseAncestorIndexes = async (app, recordInfo) => { |
||||
|
for (const index of recordInfo.recordNode.indexes) { |
||||
|
const indexKey = recordInfo.child(index.name) |
||||
|
if (!(await app.datastore.exists(indexKey))) { |
||||
|
await initialiseIndex(app.datastore, recordInfo.dir, index) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const initialiseReverseReferenceIndexes = async (app, recordInfo) => { |
||||
|
const indexNodes = $( |
||||
|
fieldsThatReferenceThisRecord(app, recordInfo.recordNode), |
||||
|
[ |
||||
|
map(f => |
||||
|
$(f.typeOptions.reverseIndexNodeKeys, [ |
||||
|
map(n => getNode(app.hierarchy, n)), |
||||
|
]) |
||||
|
), |
||||
|
flatten, |
||||
|
] |
||||
|
) |
||||
|
|
||||
|
for (const indexNode of indexNodes) { |
||||
|
await initialiseIndex(app.datastore, recordInfo.dir, indexNode) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const fieldsThatReferenceThisRecord = (app, recordNode) => |
||||
|
$(app.hierarchy, [ |
||||
|
getFlattenedHierarchy, |
||||
|
filter(isRecord), |
||||
|
map(n => n.fields), |
||||
|
flatten, |
||||
|
filter(fieldReversesReferenceToNode(recordNode)), |
||||
|
]) |
||||
|
|
||||
|
|
||||
@ -0,0 +1,52 @@ |
|||||
|
import { |
||||
|
findRoot, |
||||
|
getFlattenedHierarchy, |
||||
|
fieldReversesReferenceToIndex, |
||||
|
isRecord |
||||
|
} from "./hierarchy" |
||||
|
import { $ } from "../common" |
||||
|
import { map, filter, reduce } from "lodash/fp" |
||||
|
|
||||
|
export const canDeleteIndex = indexNode => { |
||||
|
const flatHierarchy = $(indexNode, [ |
||||
|
findRoot, |
||||
|
getFlattenedHierarchy |
||||
|
]) |
||||
|
|
||||
|
const reverseIndexes = $(flatHierarchy,[ |
||||
|
filter(isRecord), |
||||
|
reduce((obj, r) => { |
||||
|
for (let field of r.fields) { |
||||
|
if (fieldReversesReferenceToIndex(indexNode)(field)) { |
||||
|
obj.push({ ...field, record:r }) |
||||
|
} |
||||
|
} |
||||
|
return obj |
||||
|
},[]), |
||||
|
map(f => `field ${f.name} on record ${f.record.name} uses this index as a reference`) |
||||
|
]) |
||||
|
|
||||
|
const lookupIndexes = $(flatHierarchy,[ |
||||
|
filter(isRecord), |
||||
|
reduce((obj, r) => { |
||||
|
for (let field of r.fields) { |
||||
|
if (field.type === "reference" |
||||
|
&& field.typeOptions.indexNodeKey === indexNode.nodeKey()) { |
||||
|
obj.push({ ...field, record:r }) |
||||
|
} |
||||
|
} |
||||
|
return obj |
||||
|
},[]), |
||||
|
map(f => `field ${f.name} on record ${f.record.name} uses this index as a lookup`) |
||||
|
]) |
||||
|
|
||||
|
const errors = [ |
||||
|
...reverseIndexes, |
||||
|
...lookupIndexes |
||||
|
] |
||||
|
|
||||
|
return { |
||||
|
canDelete: errors.length === 0, |
||||
|
errors |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
import { |
||||
|
findRoot, |
||||
|
getFlattenedHierarchy, |
||||
|
fieldReversesReferenceToIndex, |
||||
|
isRecord, |
||||
|
isAncestorIndex, |
||||
|
isAncestor |
||||
|
} from "./hierarchy" |
||||
|
import { $ } from "../common" |
||||
|
import { map, filter, includes } from "lodash/fp" |
||||
|
|
||||
|
export const canDeleteRecord = recordNode => { |
||||
|
const flatHierarchy = $(recordNode, [ |
||||
|
findRoot, |
||||
|
getFlattenedHierarchy |
||||
|
]) |
||||
|
|
||||
|
const ancestors = $(flatHierarchy, [ |
||||
|
filter(isAncestor(recordNode)) |
||||
|
]) |
||||
|
|
||||
|
const belongsToAncestor = i => |
||||
|
ancestors.includes(i.parent()) |
||||
|
|
||||
|
|
||||
|
const errorsForNode = node => { |
||||
|
const errorsThisNode = $(flatHierarchy, [ |
||||
|
filter(i => isAncestorIndex(i) |
||||
|
&& belongsToAncestor(i) |
||||
|
&& includes(node.nodeId)(i.allowedRecordNodeIds)), |
||||
|
map(i => `index ${i.name} indexes this record. Please remove the record from allowedRecordIds, or delete the index`) |
||||
|
]) |
||||
|
|
||||
|
for (let child of node.children) { |
||||
|
for (let err of errorsForNode(child)) { |
||||
|
errorsThisNode.push(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return errorsThisNode |
||||
|
} |
||||
|
|
||||
|
return errorsForNode(recordNode) |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
import { getAllIdsIterator } from "../indexing/allIds" |
||||
|
import { getRecordInfo } from "../recordApi/recordInfo" |
||||
|
import { isTopLevelIndex, getParentKey, getLastPartInKey } from "./hierarchy" |
||||
|
import { safeKey, joinKey } from "../common" |
||||
|
|
||||
|
export const deleteAllIndexFilesForNode = async (app, indexNode) => { |
||||
|
|
||||
|
if (isTopLevelIndex(indexNode)) { |
||||
|
await app.datastore.deleteFolder(indexNode.nodeKey()) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const iterate = await getAllIdsIterator(app)(indexNode.parent().nodeKey()) |
||||
|
let iterateResult = await iterate() |
||||
|
while (!iterateResult.done) { |
||||
|
const { result } = iterateResult |
||||
|
for (const id of result.ids) { |
||||
|
const deletingIndexKey = joinKey( |
||||
|
result.collectionKey, id, indexNode.name) |
||||
|
await deleteIndexFolder(app, deletingIndexKey) |
||||
|
} |
||||
|
iterateResult = await iterate() |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const deleteIndexFolder = async (app, indexKey) => { |
||||
|
indexKey = safeKey(indexKey) |
||||
|
const indexName = getLastPartInKey(indexKey) |
||||
|
const parentRecordKey = getParentKey(indexKey) |
||||
|
const recordInfo = getRecordInfo(app.hierarchy, parentRecordKey) |
||||
|
await app.datastore.deleteFolder( |
||||
|
joinKey(recordInfo.dir, indexName)) |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
import { getAllIdsIterator } from "../indexing/allIds" |
||||
|
import { getCollectionDir } from "../recordApi/recordInfo" |
||||
|
import { isTopLevelRecord, getCollectionKey } from "./hierarchy" |
||||
|
import { safeKey, joinKey } from "../common" |
||||
|
|
||||
|
export const deleteAllRecordsForNode = async (app, recordNode) => { |
||||
|
|
||||
|
if (isTopLevelRecord(recordNode)) { |
||||
|
await deleteRecordCollection( |
||||
|
app, recordNode.collectionName) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const iterate = await getAllIdsIterator(app)(recordNode.parent().nodeKey()) |
||||
|
let iterateResult = await iterate() |
||||
|
while (!iterateResult.done) { |
||||
|
const { result } = iterateResult |
||||
|
for (const id of result.ids) { |
||||
|
const deletingCollectionKey = joinKey( |
||||
|
result.collectionKey, id, recordNode.collectionName) |
||||
|
await deleteRecordCollection(app, deletingCollectionKey) |
||||
|
} |
||||
|
iterateResult = await iterate() |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const deleteRecordCollection = async (app, collectionKey) => { |
||||
|
collectionKey = safeKey(collectionKey) |
||||
|
await app.datastore.deleteFolder( |
||||
|
getCollectionDir(app.hierarchy, collectionKey)) |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
import { getAllIdsIterator } from "../indexing/allIds" |
||||
|
import { getRecordInfo } from "../recordApi/recordInfo" |
||||
|
import { isTopLevelIndex } from "./hierarchy" |
||||
|
import { joinKey } from "../common" |
||||
|
import { initialiseIndex } from "../indexing/initialiseIndex" |
||||
|
|
||||
|
export const initialiseNewIndex = async (app, indexNode) => { |
||||
|
|
||||
|
if (isTopLevelIndex(indexNode)) { |
||||
|
await initialiseIndex(app.datastore, "/", indexNode) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const iterate = await getAllIdsIterator(app)(indexNode.parent().nodeKey()) |
||||
|
let iterateResult = await iterate() |
||||
|
while (!iterateResult.done) { |
||||
|
const { result } = iterateResult |
||||
|
for (const id of result.ids) { |
||||
|
const recordKey = joinKey(result.collectionKey, id) |
||||
|
await initialiseIndex( |
||||
|
app.datastore, |
||||
|
getRecordInfo(app.hierarchy, recordKey).dir, |
||||
|
indexNode) |
||||
|
} |
||||
|
iterateResult = await iterate() |
||||
|
} |
||||
|
} |
||||
@ -1,17 +1,194 @@ |
|||||
/* |
import { diffHierarchy, HierarchyChangeTypes } from "./diffHierarchy" |
||||
const changeActions = { |
import { $, switchCase } from "../common" |
||||
rebuildIndex: indexNodeKey => ({ |
import { |
||||
type: "rebuildIndex", |
differenceBy, |
||||
indexNodeKey, |
isEqual, |
||||
}), |
some, |
||||
reshardRecords: recordNodeKey => ({ |
map, |
||||
type: "reshardRecords", |
filter, |
||||
recordNodeKey, |
uniqBy, |
||||
}), |
flatten |
||||
deleteRecords: recordNodeKey => ({ |
} from "lodash/fp" |
||||
type: "reshardRecords", |
import { |
||||
recordNodeKey, |
findRoot, |
||||
}), |
getDependantIndexes, |
||||
renameRecord |
isTopLevelRecord, |
||||
} |
isAncestorIndex |
||||
*/ |
} from "./hierarchy" |
||||
|
import { generateSchema } from "../indexing/indexSchemaCreator" |
||||
|
import { _buildIndex } from "../indexApi/buildIndex" |
||||
|
import { constructHierarchy } from "./createNodes" |
||||
|
import { deleteAllRecordsForNode } from "./deleteAllRecordsForNode" |
||||
|
import { deleteAllIndexFilesForNode } from "./deleteAllIndexFilesForNode" |
||||
|
import { cloneApp } from "../appInitialise/cloneApp" |
||||
|
import { initialiseData } from "../appInitialise/initialiseData" |
||||
|
import { initialiseChildrenForNode } from "../recordApi/initialiseChildren" |
||||
|
import { initialiseNewIndex } from "./initialiseNewIndex" |
||||
|
import { saveApplicationHierarchy } from "../templateApi/saveApplicationHierarchy" |
||||
|
|
||||
|
export const upgradeData = app => async newHierarchy => { |
||||
|
const diff = diffHierarchy(app.hierarchy, newHierarchy) |
||||
|
const changeActions = gatherChangeActions(diff) |
||||
|
|
||||
|
if (changeActions.length === 0) return |
||||
|
|
||||
|
newHierarchy = constructHierarchy(newHierarchy) |
||||
|
const newApp = newHierarchy && cloneApp(app, { |
||||
|
hierarchy: newHierarchy |
||||
|
}) |
||||
|
await doUpgrade(app, newApp, changeActions) |
||||
|
await saveApplicationHierarchy(newApp)(newHierarchy) |
||||
|
} |
||||
|
|
||||
|
const gatherChangeActions = (diff) => |
||||
|
$(diff, [ |
||||
|
map(actionForChange), |
||||
|
flatten, |
||||
|
uniqBy(a => a.compareKey) |
||||
|
]) |
||||
|
|
||||
|
const doUpgrade = async (oldApp, newApp, changeActions) => { |
||||
|
for(let action of changeActions) { |
||||
|
await action.run(oldApp, newApp, action.diff) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const actionForChange = diff => |
||||
|
switchCase( |
||||
|
|
||||
|
[isChangeType(HierarchyChangeTypes.recordCreated), recordCreatedAction], |
||||
|
|
||||
|
[isChangeType(HierarchyChangeTypes.recordDeleted), deleteRecordsAction], |
||||
|
|
||||
|
[ |
||||
|
isChangeType(HierarchyChangeTypes.recordFieldsChanged), |
||||
|
rebuildAffectedIndexesAction |
||||
|
], |
||||
|
|
||||
|
[isChangeType(HierarchyChangeTypes.recordRenamed), renameRecordAction], |
||||
|
|
||||
|
[ |
||||
|
isChangeType(HierarchyChangeTypes.recordEstimatedRecordTypeChanged), |
||||
|
reshardRecordsAction |
||||
|
], |
||||
|
|
||||
|
[isChangeType(HierarchyChangeTypes.indexCreated), newIndexAction], |
||||
|
|
||||
|
[isChangeType(HierarchyChangeTypes.indexDeleted), deleteIndexAction], |
||||
|
|
||||
|
[isChangeType(HierarchyChangeTypes.indexChanged), rebuildIndexAction], |
||||
|
|
||||
|
)(diff) |
||||
|
|
||||
|
|
||||
|
const isChangeType = changeType => change => |
||||
|
change.type === changeType |
||||
|
|
||||
|
const action = (diff, compareKey, run) => ({ |
||||
|
diff, |
||||
|
compareKey, |
||||
|
run, |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
const reshardRecordsAction = diff => |
||||
|
[action(diff, `reshardRecords-${diff.oldNode.nodeKey()}`, runReshardRecords)] |
||||
|
|
||||
|
const rebuildIndexAction = diff => |
||||
|
[action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex)] |
||||
|
|
||||
|
const newIndexAction = diff => { |
||||
|
if (isAncestorIndex(diff.newNode)) { |
||||
|
return [action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex)] |
||||
|
} else { |
||||
|
return [action(diff, `newIndex-${diff.newNode.nodeKey()}`, runNewIndex)] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const deleteIndexAction = diff => |
||||
|
[action(diff, `deleteIndex-${diff.oldNode.nodeKey()}`, runDeleteIndex)] |
||||
|
|
||||
|
const deleteRecordsAction = diff => |
||||
|
[action(diff, `deleteRecords-${diff.oldNode.nodeKey()}`, runDeleteRecords)] |
||||
|
|
||||
|
const renameRecordAction = diff => |
||||
|
[action(diff, `renameRecords-${diff.oldNode.nodeKey()}`, runRenameRecord)] |
||||
|
|
||||
|
const recordCreatedAction = diff => { |
||||
|
if (isTopLevelRecord(diff.newNode)) { |
||||
|
return [action(diff, `initialiseRoot`, runInitialiseRoot)] |
||||
|
} |
||||
|
|
||||
|
return [action(diff, `initialiseChildRecord-${diff.newNode.nodeKey()}`, runInitialiseChildRecord)] |
||||
|
} |
||||
|
|
||||
|
const rebuildAffectedIndexesAction = diff =>{ |
||||
|
const newHierarchy = findRoot(diff.newNode) |
||||
|
const oldHierarchy = findRoot(diff.oldNode) |
||||
|
const indexes = getDependantIndexes(newHierarchy, diff.newNode) |
||||
|
|
||||
|
const changedFields = (() => { |
||||
|
const addedFields = differenceBy(f => f.name) |
||||
|
(diff.oldNode.fields) |
||||
|
(diff.newNode.fields) |
||||
|
|
||||
|
const removedFields = differenceBy(f => f.name) |
||||
|
(diff.newNode.fields) |
||||
|
(diff.oldNode.fields) |
||||
|
|
||||
|
return map(f => f.name)([...addedFields, ...removedFields]) |
||||
|
})() |
||||
|
|
||||
|
const isIndexAffected = i => { |
||||
|
if (!isEqual( |
||||
|
generateSchema(oldHierarchy, i), |
||||
|
generateSchema(newHierarchy, i))) return true |
||||
|
|
||||
|
if (some(f => indexes.filter.indexOf(`record.${f}`) > -1)(changedFields)) |
||||
|
return true |
||||
|
|
||||
|
if (some(f => indexes.getShardName.indexOf(`record.${f}`) > -1)(changedFields)) |
||||
|
return true |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return $(indexes, [ |
||||
|
filter(isIndexAffected), |
||||
|
map(i => action({ newNode:i }, `rebuildIndex-${i.nodeKey()}`, runRebuildIndex)) |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
const runReshardRecords = async change => { |
||||
|
throw new Error("Resharding of records is not supported yet") |
||||
|
} |
||||
|
|
||||
|
const runRebuildIndex = async (_, newApp, diff) => { |
||||
|
await _buildIndex(newApp, diff.newNode.nodeKey()) |
||||
|
} |
||||
|
|
||||
|
const runDeleteIndex = async (oldApp, _, diff) => { |
||||
|
await deleteAllIndexFilesForNode(oldApp, diff.oldNode) |
||||
|
} |
||||
|
|
||||
|
const runDeleteRecords = async (oldApp, _, diff) => { |
||||
|
await deleteAllRecordsForNode(oldApp, diff.oldNode) |
||||
|
} |
||||
|
|
||||
|
const runNewIndex = async (_, newApp, diff) => { |
||||
|
await initialiseNewIndex(newApp, diff.newNode) |
||||
|
} |
||||
|
|
||||
|
const runRenameRecord = change => { |
||||
|
/* |
||||
|
Going to disllow this in the builder. once a collection key is set... its done |
||||
|
*/ |
||||
|
} |
||||
|
|
||||
|
const runInitialiseRoot = async (_, newApp) => { |
||||
|
await initialiseData(newApp.datastore, newApp) |
||||
|
} |
||||
|
|
||||
|
const runInitialiseChildRecord = async (_, newApp, diff) => { |
||||
|
await initialiseChildrenForNode(newApp.datastore, diff.newNode) |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
import { cleanup } from "./cleanup" |
||||
|
|
||||
|
export const setCleanupFunc = (app, cleanupTransactions) => { |
||||
|
if (cleanupTransactions) { |
||||
|
app.cleanupTransactions = cleanupTransactions |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!app.cleanupTransactions || app.cleanupTransactions.isDefault) { |
||||
|
const newCleanup = async () => cleanup(app) |
||||
|
newCleanup.isDefault = true |
||||
|
app.cleanupTransactions = newCleanup |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
import { |
||||
|
setupApphierarchy, |
||||
|
basicAppHierarchyCreator_WithFields, |
||||
|
stubEventHandler, |
||||
|
} from "./specHelpers" |
||||
|
import { canDeleteIndex } from "../src/templateApi/canDeleteIndex" |
||||
|
import { canDeleteRecord } from "../src/templateApi/canDeleteRecord" |
||||
|
|
||||
|
describe("canDeleteIndex", () => { |
||||
|
it("should return no errors if deltion is valid", async () => { |
||||
|
const { appHierarchy } = await setupApphierarchy( |
||||
|
basicAppHierarchyCreator_WithFields |
||||
|
) |
||||
|
|
||||
|
const partnerIndex = appHierarchy.root.indexes.find(i => i.name === "partner_index") |
||||
|
|
||||
|
const result = canDeleteIndex(partnerIndex) |
||||
|
|
||||
|
expect(result.canDelete).toBe(true) |
||||
|
expect(result.errors).toEqual([]) |
||||
|
}) |
||||
|
|
||||
|
it("should return errors if index is a lookup for a reference field", async () => { |
||||
|
const { appHierarchy } = await setupApphierarchy( |
||||
|
basicAppHierarchyCreator_WithFields |
||||
|
) |
||||
|
|
||||
|
const customerIndex = appHierarchy.root.indexes.find(i => i.name === "customer_index") |
||||
|
|
||||
|
const result = canDeleteIndex(customerIndex) |
||||
|
|
||||
|
expect(result.canDelete).toBe(false) |
||||
|
expect(result.errors.length).toBe(1) |
||||
|
}) |
||||
|
|
||||
|
it("should return errors if index is a manyToOne index for a reference field", async () => { |
||||
|
const { appHierarchy } = await setupApphierarchy( |
||||
|
basicAppHierarchyCreator_WithFields |
||||
|
) |
||||
|
|
||||
|
const referredToCustomersIndex = appHierarchy.customerRecord.indexes.find(i => i.name === "referredToCustomers") |
||||
|
|
||||
|
const result = canDeleteIndex(referredToCustomersIndex) |
||||
|
|
||||
|
expect(result.canDelete).toBe(false) |
||||
|
expect(result.errors.length).toBe(1) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
describe("canDeleteRecord", () => { |
||||
|
it("should return no errors when deletion is valid", () => { |
||||
|
const { appHierarchy } = await setupApphierarchy( |
||||
|
basicAppHierarchyCreator_WithFields |
||||
|
) |
||||
|
|
||||
|
appHierarchy.root. |
||||
|
const result = canDeleteIndex(appHierarchy.customerRecord) |
||||
|
|
||||
|
expect(result.canDelete).toBe(true) |
||||
|
expect(result.errors).toEqual([]) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,244 @@ |
|||||
|
import { |
||||
|
getRecordApiFromTemplateApi, |
||||
|
getIndexApiFromTemplateApi, |
||||
|
} from "./specHelpers" |
||||
|
import { upgradeData } from "../src/templateApi/upgradeData" |
||||
|
import { setup } from "./upgradeDataSetup" |
||||
|
import { $, splitKey } from "../src/common" |
||||
|
import { keys, filter } from "lodash/fp" |
||||
|
import { _listItems } from "../src/indexApi/listItems" |
||||
|
import { _save } from "../src/recordApi/save" |
||||
|
|
||||
|
describe("upgradeData", () => { |
||||
|
|
||||
|
it("should delete all records and child records, when root record node deleted", async () => { |
||||
|
const { oldSetup, newSetup, recordApi } = await configure() |
||||
|
newSetup.root.children = newSetup.root.children.filter(n => n.name !== "contact") |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
const remainingKeys = $(recordApi._storeHandle.data, [ |
||||
|
keys, |
||||
|
filter(k => splitKey(k)[0] === "contacts"), |
||||
|
]) |
||||
|
|
||||
|
expect(remainingKeys.length).toBe(0) |
||||
|
|
||||
|
}) |
||||
|
|
||||
|
it("should not delete other root record types, when root record node deleted", async () => { |
||||
|
const { oldSetup, newSetup, recordApi } = await configure() |
||||
|
newSetup.root.children = newSetup.root.children.filter(n => n.name !== "contact") |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
const remainingKeys = $(recordApi._storeHandle.data, [ |
||||
|
keys, |
||||
|
filter(k => splitKey(k)[0] === "leads"), |
||||
|
]) |
||||
|
|
||||
|
expect(remainingKeys.length > 0).toBe(true) |
||||
|
|
||||
|
}) |
||||
|
|
||||
|
it("should delete all child records, when child record node deleted", async () => { |
||||
|
const { oldSetup, newSetup, recordApi } = await configure() |
||||
|
newSetup.contact.children = newSetup.contact.children.filter(n => n.name !== "deal") |
||||
|
|
||||
|
const startingKeys = $(recordApi._storeHandle.data, [ |
||||
|
keys, |
||||
|
filter(k => k.includes("/deals/")), |
||||
|
]) |
||||
|
|
||||
|
expect(startingKeys.length > 0).toBe(true) |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
const remainingKeys = $(recordApi._storeHandle.data, [ |
||||
|
keys, |
||||
|
filter(k => k.includes("/deals/")), |
||||
|
]) |
||||
|
|
||||
|
expect(remainingKeys.length).toBe(0) |
||||
|
}) |
||||
|
|
||||
|
it("should build a new root index", async () => { |
||||
|
const { oldSetup, newSetup } = await configure() |
||||
|
const newIndex = newSetup.templateApi.getNewIndexTemplate(newSetup.root) |
||||
|
newIndex.name = "more_contacts" |
||||
|
newIndex.allowedRecordNodeIds = [newSetup.contact.nodeId] |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
const itemsInNewIndex = await _listItems(newSetup.app, "/more_contacts") |
||||
|
|
||||
|
expect(itemsInNewIndex.length).toBe(2) |
||||
|
}) |
||||
|
|
||||
|
it("should update a root index", async () => { |
||||
|
const { oldSetup, newSetup } = await configure() |
||||
|
const contact_index = indexByName(newSetup.root, "contact_index") |
||||
|
contact_index.filter = "record.name === 'bobby'" |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
const itemsInNewIndex = await _listItems(newSetup.app, "/contact_index") |
||||
|
|
||||
|
expect(itemsInNewIndex.length).toBe(1) |
||||
|
}) |
||||
|
|
||||
|
it("should delete a root index", async () => { |
||||
|
const { oldSetup, newSetup } = await configure() |
||||
|
|
||||
|
// no exception
|
||||
|
await _listItems(newSetup.app, "/contact_index") |
||||
|
|
||||
|
newSetup.root.indexes = newSetup.root.indexes.filter(i => i.name !== "contact_index") |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
let er |
||||
|
try { |
||||
|
await _listItems(newSetup.app, "/contact_index") |
||||
|
} catch (e) { |
||||
|
er = e |
||||
|
} |
||||
|
|
||||
|
expect(er).toBeDefined() |
||||
|
}) |
||||
|
|
||||
|
it("should build a new child index", async () => { |
||||
|
const { oldSetup, newSetup, records } = await configure() |
||||
|
const newIndex = newSetup.templateApi.getNewIndexTemplate(newSetup.contact) |
||||
|
newIndex.name = "more_deals" |
||||
|
newIndex.allowedRecordNodeIds = [newSetup.deal.nodeId] |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
const itemsInNewIndex = await _listItems(newSetup.app, `${records.contact1.key}/more_deals`) |
||||
|
|
||||
|
expect(itemsInNewIndex.length).toBe(2) |
||||
|
}) |
||||
|
|
||||
|
it("should update a child index", async () => { |
||||
|
const { oldSetup, newSetup, records } = await configure() |
||||
|
const deal_index = indexByName(newSetup.contact, "deal_index") |
||||
|
deal_index.filter = "record.status === 'new'" |
||||
|
|
||||
|
let itemsInIndex = await _listItems(newSetup.app, `${records.contact1.key}/deal_index`) |
||||
|
expect(itemsInIndex.length).toBe(2) |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
itemsInIndex = await _listItems(newSetup.app, `${records.contact1.key}/deal_index`) |
||||
|
expect(itemsInIndex.length).toBe(1) |
||||
|
}) |
||||
|
|
||||
|
it("should delete a child index", async () => { |
||||
|
const { oldSetup, newSetup, records } = await configure() |
||||
|
|
||||
|
// no exception
|
||||
|
await _listItems(newSetup.app, `${records.contact1.key}/deal_index`) |
||||
|
|
||||
|
newSetup.contact.indexes = newSetup.contact.indexes.filter(i => i.name !== "deal_index") |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
let er |
||||
|
try { |
||||
|
await _listItems(newSetup.app, `${records.contact1.key}/deal_index`) |
||||
|
} catch (e) { |
||||
|
er = e |
||||
|
} |
||||
|
|
||||
|
expect(er).toBeDefined() |
||||
|
}) |
||||
|
|
||||
|
it("should build a new reference index", async () => { |
||||
|
const { oldSetup, newSetup, records, recordApi } = await configure() |
||||
|
const newIndex = newSetup.templateApi.getNewIndexTemplate(newSetup.lead) |
||||
|
newIndex.name = "contact_leads" |
||||
|
newIndex.allowedRecordNodeIds = [newSetup.lead.nodeId] |
||||
|
newIndex.indexType = "reference" |
||||
|
|
||||
|
const leadField = newSetup.templateApi.getNewField("string") |
||||
|
leadField.name = "lead" |
||||
|
leadField.type = "reference" |
||||
|
leadField.typeOptions = { |
||||
|
reverseIndexNodeKeys: [ newIndex.nodeKey() ], |
||||
|
indexNodeKey: "/lead_index", |
||||
|
displayValue: "name" |
||||
|
} |
||||
|
|
||||
|
newSetup.templateApi.addField(newSetup.contact, leadField) |
||||
|
|
||||
|
await upgradeData(oldSetup.app)(newSetup.root) |
||||
|
|
||||
|
const indexKey = `${records.lead1.key}/contact_leads` |
||||
|
|
||||
|
let itemsInNewIndex = await _listItems(newSetup.app, indexKey) |
||||
|
|
||||
|
expect(itemsInNewIndex.length).toBe(0) |
||||
|
|
||||
|
records.contact1.lead = records.lead1 |
||||
|
records.contact1.isNew = false |
||||
|
|
||||
|
await _save(newSetup.app, records.contact1) |
||||
|
|
||||
|
itemsInNewIndex = await _listItems(newSetup.app, indexKey) |
||||
|
|
||||
|
expect(itemsInNewIndex.length).toBe(1) |
||||
|
|
||||
|
}) |
||||
|
|
||||
|
}) |
||||
|
|
||||
|
const configure = async () => { |
||||
|
const oldSetup = await setup() |
||||
|
|
||||
|
const recordApi = await getRecordApiFromTemplateApi(oldSetup.templateApi) |
||||
|
const indexApi = await getIndexApiFromTemplateApi(oldSetup.templateApi) |
||||
|
|
||||
|
const newSetup = await setup(oldSetup.store) |
||||
|
|
||||
|
const records = await createSomeRecords(recordApi) |
||||
|
|
||||
|
return { oldSetup, newSetup, recordApi, records, indexApi } |
||||
|
} |
||||
|
|
||||
|
const createSomeRecords = async recordApi => { |
||||
|
const contact1 = recordApi.getNew("/contacts", "contact") |
||||
|
contact1.name = "bobby" |
||||
|
const contact2 = recordApi.getNew("/contacts", "contact") |
||||
|
contact2.name = "poppy" |
||||
|
|
||||
|
await recordApi.save(contact1) |
||||
|
await recordApi.save(contact2) |
||||
|
|
||||
|
const deal1 = recordApi.getNew(`${contact1.key}/deals`, "deal") |
||||
|
deal1.name = "big mad deal" |
||||
|
deal1.status = "new" |
||||
|
const deal2 = recordApi.getNew(`${contact1.key}/deals`, "deal") |
||||
|
deal2.name = "smaller deal" |
||||
|
deal2.status = "old" |
||||
|
const deal3 = recordApi.getNew(`${contact2.key}/deals`, "deal") |
||||
|
deal3.name = "ok deal" |
||||
|
deal3.status = "new" |
||||
|
|
||||
|
await recordApi.save(deal1) |
||||
|
await recordApi.save(deal2) |
||||
|
await recordApi.save(deal3) |
||||
|
|
||||
|
const lead1 = recordApi.getNew("/leads", "lead") |
||||
|
lead1.name = "big new lead" |
||||
|
|
||||
|
await recordApi.save(lead1) |
||||
|
|
||||
|
|
||||
|
|
||||
|
return { |
||||
|
contact1, contact2, deal1, deal2, deal3, lead1, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const indexByName = (parent, name) => parent.indexes.find(i => i.name === name) |
||||
@ -0,0 +1,47 @@ |
|||||
|
import { getMemoryTemplateApi, appFromTempalteApi } from "./specHelpers" |
||||
|
import { getFlattenedHierarchy } from "../src/templateApi/hierarchy" |
||||
|
import { initialiseData } from "../src/appInitialise/initialiseData" |
||||
|
|
||||
|
export const setup = async (store) => { |
||||
|
const { templateApi } = await getMemoryTemplateApi(store) |
||||
|
const root = templateApi.getNewRootLevel() |
||||
|
const contact = templateApi.getNewRecordTemplate(root, "contact", true) |
||||
|
contact.collectionName = "contacts" |
||||
|
|
||||
|
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) |
||||
|
lead.collectionName = "leads" |
||||
|
const deal = templateApi.getNewRecordTemplate(contact, "deal", true) |
||||
|
deal.collectionName = "deals" |
||||
|
|
||||
|
templateApi.addField(deal, {...nameField}) |
||||
|
templateApi.addField(deal, {...statusField}) |
||||
|
|
||||
|
templateApi.addField(lead, {...nameField}) |
||||
|
|
||||
|
getFlattenedHierarchy(root) |
||||
|
|
||||
|
if (!store) |
||||
|
await initialiseData(templateApi._storeHandle, { |
||||
|
hierarchy: root, |
||||
|
actions: [], |
||||
|
triggers: [], |
||||
|
}) |
||||
|
const app = await appFromTempalteApi(templateApi) |
||||
|
app.hierarchy = root |
||||
|
|
||||
|
return { |
||||
|
root, contact, lead, app, |
||||
|
deal, templateApi, store: templateApi._storeHandle, |
||||
|
all_contacts: root.indexes[0], |
||||
|
all_leads: root.indexes[1], |
||||
|
deals_for_contacts: contact.indexes[0], |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
const StatusCodes = require("../../utilities/statusCodes") |
||||
|
|
||||
|
module.exports = async ctx => { |
||||
|
await ctx.instance.templateApi.upgradeData(ctx.request.body.newHierarchy) |
||||
|
ctx.response.status = StatusCodes.OK |
||||
|
} |
||||
Loading…
Reference in new issue