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 @@ |
|||
/* |
|||
const changeActions = { |
|||
rebuildIndex: indexNodeKey => ({ |
|||
type: "rebuildIndex", |
|||
indexNodeKey, |
|||
}), |
|||
reshardRecords: recordNodeKey => ({ |
|||
type: "reshardRecords", |
|||
recordNodeKey, |
|||
}), |
|||
deleteRecords: recordNodeKey => ({ |
|||
type: "reshardRecords", |
|||
recordNodeKey, |
|||
}), |
|||
renameRecord |
|||
} |
|||
*/ |
|||
import { diffHierarchy, HierarchyChangeTypes } from "./diffHierarchy" |
|||
import { $, switchCase } from "../common" |
|||
import { |
|||
differenceBy, |
|||
isEqual, |
|||
some, |
|||
map, |
|||
filter, |
|||
uniqBy, |
|||
flatten |
|||
} from "lodash/fp" |
|||
import { |
|||
findRoot, |
|||
getDependantIndexes, |
|||
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