forked from tsai/budibase
48 changed files with 2925 additions and 2213 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,20 @@ |
|||||
|
import { getRecordInfo } from "../recordApi/recordInfo"; |
||||
|
import { |
||||
|
getParentKey, getLastPartInKey |
||||
|
} from "../templateApi/hierarchy"; |
||||
|
import { keySep } from "../common"; |
||||
|
|
||||
|
export const getIndexDir = (hierarchy, indexKey) => { |
||||
|
|
||||
|
const parentKey = getParentKey(indexKey); |
||||
|
|
||||
|
if(parentKey === "") return indexKey; |
||||
|
if(parentKey === keySep) return indexKey; |
||||
|
|
||||
|
const recordInfo = getRecordInfo( |
||||
|
hierarchy, |
||||
|
parentKey); |
||||
|
|
||||
|
return recordInfo.child( |
||||
|
getLastPartInKey(indexKey)); |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
import { |
||||
|
getExactNodeForKey, getActualKeyOfParent, |
||||
|
isRoot, isSingleRecord, getNodeForCollectionPath |
||||
|
} from '../templateApi/hierarchy'; |
||||
|
import { |
||||
|
reduce, find, filter, take |
||||
|
} from 'lodash/fp'; |
||||
|
import { |
||||
|
$, getFileFromKey, joinKey, safeKey, keySep |
||||
|
} from '../common'; |
||||
|
import { |
||||
|
folderStructureArray, allIdChars |
||||
|
} from "../indexing/allIds"; |
||||
|
|
||||
|
export const getRecordInfo = (hierarchy, key) => { |
||||
|
const recordNode = getExactNodeForKey(hierarchy)(key); |
||||
|
const pathInfo = getRecordDirectory(recordNode, key); |
||||
|
const dir = joinKey(pathInfo.base, ...pathInfo.subdirs); |
||||
|
|
||||
|
return { |
||||
|
recordJson: recordJson(dir), |
||||
|
files: files(dir), |
||||
|
child:(name) => joinKey(dir, name), |
||||
|
key: safeKey(key), |
||||
|
recordNode, pathInfo, dir |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export const getCollectionDir = (hierarchy, collectionKey) => { |
||||
|
const recordNode = getNodeForCollectionPath(hierarchy)(collectionKey); |
||||
|
const dummyRecordKey = joinKey(collectionKey, "1-abcd"); |
||||
|
const pathInfo = getRecordDirectory(recordNode, dummyRecordKey); |
||||
|
return pathInfo.base; |
||||
|
} |
||||
|
|
||||
|
const recordJson = (dir) => |
||||
|
joinKey(dir, "record.json") |
||||
|
|
||||
|
const files = (dir) => |
||||
|
joinKey(dir, "files") |
||||
|
|
||||
|
const getRecordDirectory = (recordNode, key) => { |
||||
|
const id = getFileFromKey(key); |
||||
|
|
||||
|
const traverseParentKeys = (n, parents=[]) => { |
||||
|
if(isRoot(n)) return parents; |
||||
|
const k = getActualKeyOfParent(n.nodeKey(), key); |
||||
|
const thisNodeDir = { |
||||
|
node:n, |
||||
|
relativeDir: joinKey( |
||||
|
recordRelativeDirectory(n, getFileFromKey(k))) |
||||
|
}; |
||||
|
return traverseParentKeys( |
||||
|
n.parent(), |
||||
|
[thisNodeDir, ...parents]); |
||||
|
} |
||||
|
|
||||
|
const parentDirs = $(recordNode.parent(), [ |
||||
|
traverseParentKeys, |
||||
|
reduce((key, item) => { |
||||
|
return joinKey(key, item.node.collectionName, item.relativeDir) |
||||
|
}, keySep) |
||||
|
]); |
||||
|
|
||||
|
const subdirs = isSingleRecord(recordNode) |
||||
|
? [] |
||||
|
: recordRelativeDirectory(recordNode, id); |
||||
|
const base = isSingleRecord(recordNode) |
||||
|
? joinKey(parentDirs, recordNode.name) |
||||
|
: joinKey(parentDirs, recordNode.collectionName); |
||||
|
|
||||
|
return ({ |
||||
|
subdirs, base |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
const recordRelativeDirectory = (recordNode, id) => { |
||||
|
const folderStructure = folderStructureArray(recordNode); |
||||
|
const strippedId = id.substring(recordNode.nodeId.toString().length + 1); |
||||
|
const subfolders = $(folderStructure, [ |
||||
|
reduce((result, currentCount) => { |
||||
|
result.folders.push( |
||||
|
folderForChar(strippedId[result.level], currentCount) |
||||
|
); |
||||
|
return {level:result.level+1, folders:result.folders}; |
||||
|
}, {level:0, folders:[]}), |
||||
|
f => f.folders, |
||||
|
filter(f => !!f) |
||||
|
]); |
||||
|
|
||||
|
return [recordNode.nodeId.toString(), ...subfolders, id] |
||||
|
} |
||||
|
|
||||
|
const folderForChar = (char, folderCount) => |
||||
|
folderCount === 1 ? "" |
||||
|
: $(folderCount, [ |
||||
|
idFoldersForFolderCount, |
||||
|
find(f => f.includes(char)) |
||||
|
]); |
||||
|
|
||||
|
const idFoldersForFolderCount = (folderCount) => { |
||||
|
const charRangePerShard = 64 / folderCount; |
||||
|
const idFolders = []; |
||||
|
let index = 0; |
||||
|
let currentIdsShard = ''; |
||||
|
while (index < 64) { |
||||
|
currentIdsShard += allIdChars[index]; |
||||
|
if ((index + 1) % charRangePerShard === 0) { |
||||
|
idFolders.push(currentIdsShard); |
||||
|
currentIdsShard = ''; |
||||
|
} |
||||
|
index++; |
||||
|
} |
||||
|
|
||||
|
return idFolders; |
||||
|
}; |
||||
|
|
||||
@ -0,0 +1,93 @@ |
|||||
|
import {setupApphierarchy, basicAppHierarchyCreator_WithFields, |
||||
|
getNewFieldAndAdd} from "./specHelpers"; |
||||
|
import {isNonEmptyString} from "../src/common"; |
||||
|
import { isBoolean } from "util"; |
||||
|
import {permission} from "../src/authApi/permissions"; |
||||
|
import { _getNew } from "../src/recordApi/getNew"; |
||||
|
|
||||
|
describe("recordApi > getNew", () => { |
||||
|
|
||||
|
it("should get object with generated id and key (full path)", async () => { |
||||
|
const {recordApi} = await setupApphierarchy(basicAppHierarchyCreator_WithFields); |
||||
|
const record = recordApi.getNew("/customers", "customer"); |
||||
|
|
||||
|
expect(record.id).toBeDefined(); |
||||
|
expect(isNonEmptyString(record.id)).toBeTruthy(); |
||||
|
|
||||
|
expect(record.key).toBeDefined(); |
||||
|
expect(isNonEmptyString(record.key)).toBeTruthy(); |
||||
|
expect(record.key).toBe(`/customers/${record.id}`); |
||||
|
}); |
||||
|
|
||||
|
it("should create object with all declared fields, using default values", async () => { |
||||
|
const {recordApi} = await setupApphierarchy(basicAppHierarchyCreator_WithFields); |
||||
|
|
||||
|
const newRecord = recordApi.getNew("/customers", "customer") |
||||
|
|
||||
|
expect(newRecord.surname).toBe(null); |
||||
|
expect(newRecord.isalive).toBe(true); |
||||
|
expect(newRecord.createddate).toBe(null); |
||||
|
expect(newRecord.age).toBe(null); |
||||
|
}); |
||||
|
|
||||
|
it("should create object with all declared fields, and use inital values", async () => { |
||||
|
const {recordApi} = await setupApphierarchy(templateApi => { |
||||
|
const hierarchy = basicAppHierarchyCreator_WithFields(templateApi); |
||||
|
const {customerRecord} = hierarchy; |
||||
|
|
||||
|
customerRecord.fields = []; |
||||
|
|
||||
|
const newField = getNewFieldAndAdd(templateApi, customerRecord); |
||||
|
newField("surname", "string", "hello"); |
||||
|
newField("isalive", "bool", "true"); |
||||
|
newField("age", "number", "999"); |
||||
|
|
||||
|
return hierarchy; |
||||
|
}); |
||||
|
|
||||
|
const newRecord = recordApi.getNew("/customers", "customer") |
||||
|
|
||||
|
expect(newRecord.surname).toBe("hello"); |
||||
|
expect(newRecord.isalive).toBe(true); |
||||
|
expect(newRecord.age).toBe(999); |
||||
|
}); |
||||
|
|
||||
|
it("should add a function 'isNew' which always returns true", async () => { |
||||
|
const {recordApi} = await setupApphierarchy(basicAppHierarchyCreator_WithFields); |
||||
|
const record = recordApi.getNew("/customers", "customer"); |
||||
|
|
||||
|
expect(record.isNew).toBeDefined(); |
||||
|
expect(isBoolean(record.isNew)).toBeTruthy(); |
||||
|
expect(record.isNew).toBeTruthy(); |
||||
|
}); |
||||
|
|
||||
|
it("should add a function 'type' returns type", async () => { |
||||
|
const {recordApi} = await setupApphierarchy(basicAppHierarchyCreator_WithFields); |
||||
|
const record = recordApi.getNew("/customers", "customer"); |
||||
|
|
||||
|
expect(record.type).toBeDefined(); |
||||
|
expect(isNonEmptyString(record.type)).toBeTruthy(); |
||||
|
expect(record.type).toBe("customer"); |
||||
|
}); |
||||
|
|
||||
|
it("should throw error, user user does not have permission", async () => { |
||||
|
const {recordApi, app, appHierarchy} = await setupApphierarchy(basicAppHierarchyCreator_WithFields); |
||||
|
app.removePermission(permission.createRecord.get(appHierarchy.customerRecord.nodeKey())); |
||||
|
expect(() => recordApi.getNew("/customers", "customer")).toThrow(/Unauthorized/); |
||||
|
}); |
||||
|
|
||||
|
it("should not depend on having any other permissions", async () => { |
||||
|
const {recordApi, app, appHierarchy} = await setupApphierarchy(basicAppHierarchyCreator_WithFields); |
||||
|
app.withOnlyThisPermission(permission.createRecord.get(appHierarchy.customerRecord.nodeKey())); |
||||
|
recordApi.getNew("/customers", "customer"); |
||||
|
}); |
||||
|
|
||||
|
it("for 'single record' type, should create with key ending in node name", async () => { |
||||
|
const {appHierarchy} = await setupApphierarchy(basicAppHierarchyCreator_WithFields); |
||||
|
const {settingsRecord} = appHierarchy; |
||||
|
const result = _getNew(settingsRecord, ""); |
||||
|
expect(result.key).toBe("/settings") |
||||
|
}) |
||||
|
}); |
||||
|
|
||||
|
|
||||
@ -0,0 +1,124 @@ |
|||||
|
import { folderStructureArray } from "../src/indexing/allIds"; |
||||
|
import { getRecordInfo } from "../src/recordApi/recordInfo"; |
||||
|
import {setupApphierarchy} from "./specHelpers"; |
||||
|
|
||||
|
describe("getRecordInfo", () => { |
||||
|
|
||||
|
it("dir should not be sharded when record count = 1000", async () => { |
||||
|
const {root} = (await setup({parentCount: 1000})).appHierarchy; |
||||
|
const {dir} = getRecordInfo(root, "/parents/1-abcd"); |
||||
|
expect(dir).toBe("/parents/1/1-abcd"); |
||||
|
}); |
||||
|
|
||||
|
it("dir should be sharded when record count = 1001", async () => { |
||||
|
const {root} = (await setup({parentCount: 1001})).appHierarchy; |
||||
|
const {dir} = getRecordInfo(root, "/parents/1-abcd"); |
||||
|
expect(dir).toBe("/parents/1/0123456789abcdefghijklmnopqrstuv/1-abcd"); |
||||
|
}); |
||||
|
|
||||
|
it("dir should be sharded to one char per folder when record count = 63,000 (64*1000)", async () => { |
||||
|
const {root} = (await setup({parentCount: 64000})).appHierarchy; |
||||
|
const {dir} = getRecordInfo(root, "/parents/1-abcd"); |
||||
|
expect(dir).toBe("/parents/1/a/1-abcd"); |
||||
|
}); |
||||
|
|
||||
|
it("dir should be sharded to one char per folder, on 2 levels when record count = 4096000 (64*64*1000)", async () => { |
||||
|
const {root} = (await setup({parentCount: 4096000})).appHierarchy; |
||||
|
const {dir} = getRecordInfo(root, "/parents/1-abcd"); |
||||
|
expect(dir).toBe("/parents/1/a/b/1-abcd"); |
||||
|
}); |
||||
|
|
||||
|
it("child dir should not be sharded when record count = 1000", async () => { |
||||
|
const {root, child} = (await setup({parentCount: 4096000, childCount: 1000})).appHierarchy; |
||||
|
const {dir} = getRecordInfo(root, `/parents/1-abcd/children/${child.nodeId}-defg`); |
||||
|
expect(dir).toBe(`/parents/1/a/b/1-abcd/children/${child.nodeId}/${child.nodeId}-defg`); |
||||
|
}); |
||||
|
|
||||
|
it("grandchild dir should not be sharded when record count = 1000", async () => { |
||||
|
const {root, child, grandchild} = (await setup({parentCount: 4096000, childCount: 4096000})).appHierarchy; |
||||
|
const {dir} = getRecordInfo(root, `/parents/1-abcd/children/${child.nodeId}-defg/grandchildren/${grandchild.nodeId}-hijk`); |
||||
|
expect(dir).toBe(`/parents/1/a/b/1-abcd/children/${child.nodeId}/d/e/${child.nodeId}-defg/grandchildren/${grandchild.nodeId}/${grandchild.nodeId}-hijk`); |
||||
|
}); |
||||
|
|
||||
|
it("grandchild dir should be sharded when record count = 4096000", async () => { |
||||
|
const {root, child, grandchild} = (await setup({parentCount: 4096000, childCount: 4096000, grandChildCount: 4096000})).appHierarchy; |
||||
|
const {dir} = getRecordInfo(root, `/parents/1-abcd/children/${child.nodeId}-defg/grandchildren/${grandchild.nodeId}-hijk`); |
||||
|
expect(dir).toBe(`/parents/1/a/b/1-abcd/children/${child.nodeId}/d/e/${child.nodeId}-defg/grandchildren/${grandchild.nodeId}/h/i/${grandchild.nodeId}-hijk`); |
||||
|
}); |
||||
|
|
||||
|
it("child levels can be sharded, with parent not", async () => { |
||||
|
const {root, child, grandchild} = (await setup({parentCount: 1000, childCount: 4096000, grandChildCount: 4096000})).appHierarchy; |
||||
|
const {dir} = getRecordInfo(root, `/parents/1-abcd/children/${child.nodeId}-defg/grandchildren/${grandchild.nodeId}-hijk`); |
||||
|
expect(dir).toBe(`/parents/1/1-abcd/children/${child.nodeId}/d/e/${child.nodeId}-defg/grandchildren/${grandchild.nodeId}/h/i/${grandchild.nodeId}-hijk`); |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
|
||||
|
describe("folderStructureArray", () => { |
||||
|
|
||||
|
const recordNode = (count) => ({estimatedRecordCount: count}); |
||||
|
|
||||
|
it("should return [] when folder count < 1000", () => { |
||||
|
const result = folderStructureArray(recordNode(999)); |
||||
|
expect(result).toEqual([]); |
||||
|
}); |
||||
|
|
||||
|
it("should return [4] when folder count between 3000 - 4000", () => { |
||||
|
const result = folderStructureArray(recordNode(3456)); |
||||
|
expect(result).toEqual([4]); |
||||
|
}) |
||||
|
|
||||
|
it("should return [64, 2] when folder count between 64000 - 65000", () => { |
||||
|
const result = folderStructureArray(recordNode(64001)); |
||||
|
expect(result).toEqual([64, 2]); |
||||
|
}) |
||||
|
|
||||
|
it("should return [64, 64] when folder = 4095999", () => { |
||||
|
const result = folderStructureArray(recordNode(4095999)); |
||||
|
expect(result).toEqual([64, 64]); |
||||
|
}); |
||||
|
|
||||
|
it("should return [64, 64] when folder = 4096000", () => { |
||||
|
const result = folderStructureArray(recordNode(4096000)); |
||||
|
expect(result).toEqual([64, 64]); |
||||
|
}); |
||||
|
|
||||
|
it("should return [64, 64, 2] when folder = 4096001", () => { |
||||
|
const result = folderStructureArray(recordNode(4096001)); |
||||
|
expect(result).toEqual([64, 64, 2]); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
const setup = ({parentCount, childCount, grandChildCount}) => |
||||
|
setupApphierarchy((templateApi) => { |
||||
|
|
||||
|
const root = templateApi.getNewRootLevel(); |
||||
|
|
||||
|
const addField = (recordNode) => { |
||||
|
const field = templateApi.getNewField("string"); |
||||
|
field.name = "test"; |
||||
|
templateApi.addField(recordNode, field); |
||||
|
return field; |
||||
|
}; |
||||
|
|
||||
|
const parent = templateApi.getNewRecordTemplate(root, "parent"); |
||||
|
parent.estimatedRecordCount = parentCount || 1000; |
||||
|
parent.collectionName = "parents"; |
||||
|
addField(parent); |
||||
|
const child = templateApi.getNewRecordTemplate(parent, "child"); |
||||
|
child.estimatedRecordCount = childCount || 1000; |
||||
|
child.collectionName = "children"; |
||||
|
addField(child); |
||||
|
const grandchild = templateApi.getNewRecordTemplate(child, "grandchild"); |
||||
|
grandchild.estimatedRecordCount = grandChildCount || 1000; |
||||
|
grandchild.collectionName = "grandchildren"; |
||||
|
addField(grandchild); |
||||
|
|
||||
|
return ({ |
||||
|
parent, child, grandchild, root |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue