@budibase/core
Version:
core javascript library for budibase
1,929 lines (1,630 loc) • 972 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var fp = require('lodash/fp');
var shortid = require('shortid');
var _ = require('lodash');
var ___default = _interopDefault(_);
var bcrypt = _interopDefault(require('bcryptjs'));
var compilerUtil = require('@nx-js/compiler-util');
var lunr = _interopDefault(require('lunr'));
var safeBuffer = require('safe-buffer');
const commonPlus = extra => fp.union(["onBegin", "onComplete", "onError"])(extra);
const common = () => commonPlus([]);
const _events = {
recordApi: {
save: commonPlus(["onInvalid", "onRecordUpdated", "onRecordCreated"]),
delete: common(),
getContext: common(),
getNew: common(),
load: common(),
validate: common(),
uploadFile: common(),
downloadFile: common(),
},
indexApi: {
buildIndex: common(),
listItems: common(),
delete: common(),
aggregates: common(),
},
collectionApi: {
getAllowedRecordTypes: common(),
initialise: common(),
delete: common(),
},
authApi: {
authenticate: common(),
authenticateTemporaryAccess: common(),
createTemporaryAccess: common(),
createUser: common(),
enableUser: common(),
disableUser: common(),
loadAccessLevels: common(),
getNewAccessLevel: common(),
getNewUser: common(),
getNewUserAuth: common(),
getUsers: common(),
saveAccessLevels: common(),
isAuthorized: common(),
changeMyPassword: common(),
setPasswordFromTemporaryCode: common(),
scorePassword: common(),
isValidPassword: common(),
validateUser: common(),
validateAccessLevels: common(),
setUserAccessLevels: common(),
},
templateApi: {
saveApplicationHierarchy: common(),
saveActionsAndTriggers: common(),
},
actionsApi: {
execute: common(),
},
};
const _eventsList = [];
const makeEvent = (area, method, name) => `${area}:${method}:${name}`;
for (const areaKey in _events) {
for (const methodKey in _events[areaKey]) {
_events[areaKey][methodKey] = fp.reduce((obj, s) => {
obj[s] = makeEvent(areaKey, methodKey, s);
return obj
}, {})(_events[areaKey][methodKey]);
}
}
for (const areaKey in _events) {
for (const methodKey in _events[areaKey]) {
for (const name in _events[areaKey][methodKey]) {
_eventsList.push(_events[areaKey][methodKey][name]);
}
}
}
const events = _events;
const eventsList = _eventsList;
class BadRequestError extends Error {
constructor(message) {
super(message);
this.httpStatusCode = 400;
}
}
class UnauthorisedError extends Error {
constructor(message) {
super(message);
this.httpStatusCode = 401;
}
}
class ForbiddenError extends Error {
constructor(message) {
super(message);
this.httpStatusCode = 403;
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.httpStatusCode = 404;
}
}
const apiWrapper = async (
app,
eventNamespace,
isAuthorized,
eventContext,
func,
...params
) => {
pushCallStack(app, eventNamespace);
if (!isAuthorized(app)) {
handleNotAuthorized(app, eventContext, eventNamespace);
return
}
const startDate = Date.now();
const elapsed = () => Date.now() - startDate;
try {
await app.publish(eventNamespace.onBegin, eventContext);
const result = await func(...params);
await publishComplete(app, eventContext, eventNamespace, elapsed, result);
return result
} catch (error) {
await publishError(app, eventContext, eventNamespace, elapsed, error);
throw error
}
};
const apiWrapperSync = (
app,
eventNamespace,
isAuthorized,
eventContext,
func,
...params
) => {
pushCallStack(app, eventNamespace);
if (!isAuthorized(app)) {
handleNotAuthorized(app, eventContext, eventNamespace);
return
}
const startDate = Date.now();
const elapsed = () => Date.now() - startDate;
try {
app.publish(eventNamespace.onBegin, eventContext);
const result = func(...params);
publishComplete(app, eventContext, eventNamespace, elapsed, result);
return result
} catch (error) {
publishError(app, eventContext, eventNamespace, elapsed, error);
throw error
}
};
const handleNotAuthorized = (app, eventContext, eventNamespace) => {
const err = new UnauthorisedError(`Unauthorized: ${eventNamespace}`);
publishError(app, eventContext, eventNamespace, () => 0, err);
throw err
};
const pushCallStack = (app, eventNamespace, seedCallId) => {
const callId = shortid.generate();
const createCallStack = () => ({
seedCallId: !fp.isUndefined(seedCallId) ? seedCallId : callId,
threadCallId: callId,
stack: [],
});
if (fp.isUndefined(app.calls)) {
app.calls = createCallStack();
}
app.calls.stack.push({
namespace: eventNamespace,
callId,
});
};
const popCallStack = app => {
app.calls.stack.pop();
if (app.calls.stack.length === 0) {
delete app.calls;
}
};
const publishError = async (
app,
eventContext,
eventNamespace,
elapsed,
err
) => {
const ctx = fp.cloneDeep(eventContext);
ctx.error = err;
ctx.elapsed = elapsed();
await app.publish(eventNamespace.onError, ctx);
popCallStack(app);
};
const publishComplete = async (
app,
eventContext,
eventNamespace,
elapsed,
result
) => {
const endcontext = fp.cloneDeep(eventContext);
endcontext.result = result;
endcontext.elapsed = elapsed();
await app.publish(eventNamespace.onComplete, endcontext);
popCallStack(app);
return result
};
const lockOverlapMilliseconds = 10;
const getLock = async (
app,
lockFile,
timeoutMilliseconds,
maxLockRetries,
retryCount = 0
) => {
try {
const timeout = (await app.getEpochTime()) + timeoutMilliseconds;
const lock = {
timeout,
key: lockFile,
totalTimeout: timeoutMilliseconds,
};
await app.datastore.createFile(
lockFile,
getLockFileContent(lock.totalTimeout, lock.timeout)
);
return lock
} catch (e) {
if (retryCount == maxLockRetries) {
return NO_LOCK
}
const lock = parseLockFileContent(
lockFile,
await app.datastore.loadFile(lockFile)
);
const currentEpochTime = await app.getEpochTime();
if (currentEpochTime < lock.timeout) {
return NO_LOCK
}
try {
await app.datastore.deleteFile(lockFile);
} catch (_) {
//empty
}
await sleepForRetry();
return await getLock(
app,
lockFile,
timeoutMilliseconds,
maxLockRetries,
retryCount + 1
)
}
};
const getLockFileContent = (totalTimeout, epochTime) =>
`${totalTimeout}:${epochTime.toString()}`;
const parseLockFileContent = (key, content) =>
$(content, [
fp.split(":"),
parts => ({
totalTimeout: new Number(parts[0]),
timeout: new Number(parts[1]),
key,
}),
]);
const releaseLock = async (app, lock) => {
const currentEpochTime = await app.getEpochTime();
// only release if not timedout
if (currentEpochTime < lock.timeout - lockOverlapMilliseconds) {
try {
await app.datastore.deleteFile(lock.key);
} catch (_) {
//empty
}
}
};
const NO_LOCK = "no lock";
const isNolock = id => id === NO_LOCK;
const sleepForRetry = () =>
new Promise(resolve => setTimeout(resolve, lockOverlapMilliseconds));
function hash(password) {
return bcrypt.hashSync(password, 10)
}
function verify(hash, password) {
return bcrypt.compareSync(password, hash)
}
var nodeCrypto = {
hash,
verify,
};
// this is the combinator function
const $$ = (...funcs) => arg => _.flow(funcs)(arg);
// this is the pipe function
const $ = (arg, funcs) => $$(...funcs)(arg);
const keySep = "/";
const trimKeySep = str => _.trim(str, keySep);
const splitByKeySep = str => fp.split(keySep)(str);
const safeKey = key =>
_.replace(`${keySep}${trimKeySep(key)}`, `${keySep}${keySep}`, keySep);
const joinKey = (...strs) => {
const paramsOrArray = (strs.length === 1) & fp.isArray(strs[0]) ? strs[0] : strs;
return $(paramsOrArray, [
fp.filter(s => !fp.isUndefined(s) && !fp.isNull(s) && s.toString().length > 0),
fp.join(keySep),
safeKey,
])
};
const splitKey = $$(trimKeySep, splitByKeySep);
const getDirFomKey = $$(splitKey, _.dropRight, p => joinKey(...p));
const getFileFromKey = $$(splitKey, _.takeRight, _.head);
const configFolder = `${keySep}.config`;
const fieldDefinitions = joinKey(configFolder, "fields.json");
const templateDefinitions = joinKey(configFolder, "templates.json");
const appDefinitionFile = joinKey(configFolder, "appDefinition.json");
const dirIndex = folderPath =>
joinKey(configFolder, "dir", ...splitKey(folderPath), "dir.idx");
const getIndexKeyFromFileKey = $$(getDirFomKey, dirIndex);
const ifExists = (val, exists, notExists) =>
fp.isUndefined(val)
? fp.isUndefined(notExists)
? (() => {})()
: notExists()
: exists();
const getOrDefault = (val, defaultVal) =>
ifExists(
val,
() => val,
() => defaultVal
);
const not = func => val => !func(val);
const isDefined = not(fp.isUndefined);
const isNonNull = not(fp.isNull);
const isNotNaN = not(fp.isNaN);
const allTrue = (...funcArgs) => val =>
fp.reduce(
(result, conditionFunc) =>
(fp.isNull(result) || result == true) && conditionFunc(val),
null
)(funcArgs);
const anyTrue = (...funcArgs) => val =>
fp.reduce(
(result, conditionFunc) => result == true || conditionFunc(val),
null
)(funcArgs);
const insensitiveEquals = (str1, str2) =>
str1.trim().toLowerCase() === str2.trim().toLowerCase();
const isSomething = allTrue(isDefined, isNonNull, isNotNaN);
const isNothing = not(isSomething);
const isNothingOrEmpty = v => isNothing(v) || fp.isEmpty(v);
const somethingOrGetDefault = getDefaultFunc => val =>
isSomething(val) ? val : getDefaultFunc();
const somethingOrDefault = (val, defaultVal) =>
somethingOrGetDefault(fp.constant(defaultVal))(val);
const mapIfSomethingOrDefault = (mapFunc, defaultVal) => val =>
isSomething(val) ? mapFunc(val) : defaultVal;
const mapIfSomethingOrBlank = mapFunc =>
mapIfSomethingOrDefault(mapFunc, "");
const none = predicate => collection => !fp.some(predicate)(collection);
const all = predicate => collection =>
none(v => !predicate(v))(collection);
const isNotEmpty = ob => !fp.isEmpty(ob);
const isNonEmptyArray = allTrue(fp.isArray, isNotEmpty);
const isNonEmptyString = allTrue(fp.isString, isNotEmpty);
const tryOr = failFunc => (func, ...args) => {
try {
return func.apply(null, ...args)
} catch (_) {
return failFunc()
}
};
const tryAwaitOr = failFunc => async (func, ...args) => {
try {
return await func.apply(null, ...args)
} catch (_) {
return await failFunc()
}
};
const defineError = (func, errorPrefix) => {
try {
return func()
} catch (err) {
err.message = `${errorPrefix} : ${err.message}`;
throw err
}
};
const tryOrIgnore = tryOr(() => {});
const tryAwaitOrIgnore = tryAwaitOr(async () => {});
const causesException = func => {
try {
func();
return false
} catch (e) {
return true
}
};
const executesWithoutException = func => !causesException(func);
const handleErrorWith = returnValInError =>
tryOr(fp.constant(returnValInError));
const handleErrorWithUndefined = handleErrorWith(undefined);
const switchCase = (...cases) => value => {
const nextCase = () => _.head(cases)[0](value);
const nextResult = () => _.head(cases)[1](value);
if (fp.isEmpty(cases)) return // undefined
if (nextCase() === true) return nextResult()
return switchCase(..._.tail(cases))(value)
};
const isValue = val1 => val2 => val1 === val2;
const isOneOf = (...vals) => val => fp.includes(val)(vals);
const defaultCase = fp.constant(true);
const memberMatches = (member, match) => obj => match(obj[member]);
const StartsWith = searchFor => searchIn =>
_.startsWith(searchIn, searchFor);
const contains = val => array => _.findIndex(array, v => v === val) > -1;
const getHashCode = s => {
let hash = 0;
let i;
let char;
let l;
if (s.length == 0) return hash
for (i = 0, l = s.length; i < l; i++) {
char = s.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0; // Convert to 32bit integer
}
// converting to string, but dont want a "-" prefixed
if (hash < 0) {
return `n${(hash * -1).toString()}`
}
return hash.toString()
};
// thanks to https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/
const awEx = async promise => {
try {
const result = await promise;
return [undefined, result]
} catch (error) {
return [error, undefined]
}
};
const isSafeInteger = n =>
fp.isInteger(n) &&
n <= Number.MAX_SAFE_INTEGER &&
n >= 0 - Number.MAX_SAFE_INTEGER;
const toDateOrNull = s =>
fp.isNull(s) ? null : fp.isDate(s) ? s : new Date(s);
const toBoolOrNull = s => (fp.isNull(s) ? null : s === "true" || s === true);
const toNumberOrNull = s => (fp.isNull(s) ? null : fp.toNumber(s));
const isArrayOfString = opts => fp.isArray(opts) && all(fp.isString)(opts);
const pushAll = (target, items) => {
for (let i of items) target.push(i);
};
const pause = async duration =>
new Promise(res => setTimeout(res, duration));
const retry = async (fn, retries, delay, ...args) => {
try {
return await fn(...args)
} catch (err) {
if (retries > 1) {
return await pause(delay).then(
async () => await retry(fn, retries - 1, delay, ...args)
)
}
throw err
}
};
var index = {
ifExists,
getOrDefault,
isDefined,
isNonNull,
isNotNaN,
allTrue,
isSomething,
mapIfSomethingOrDefault,
mapIfSomethingOrBlank,
configFolder,
fieldDefinitions,
isNothing,
not,
switchCase,
defaultCase,
StartsWith,
contains,
templateDefinitions,
handleErrorWith,
handleErrorWithUndefined,
tryOr,
tryOrIgnore,
tryAwaitOr,
tryAwaitOrIgnore,
dirIndex,
keySep,
$,
$$,
getDirFomKey,
getFileFromKey,
splitKey,
somethingOrDefault,
getIndexKeyFromFileKey,
joinKey,
somethingOrGetDefault,
appDefinitionFile,
isValue,
all,
isOneOf,
memberMatches,
defineError,
anyTrue,
isNonEmptyArray,
causesException,
executesWithoutException,
none,
getHashCode,
awEx,
apiWrapper,
events,
eventsList,
isNothingOrEmpty,
isSafeInteger,
toNumber: fp.toNumber,
toDate: toDateOrNull,
toBool: toBoolOrNull,
isArrayOfString,
getLock,
NO_LOCK,
isNolock,
insensitiveEquals,
pause,
retry,
pushAll,
};
const stringNotEmpty = s => isSomething(s) && s.trim().length > 0;
const makerule = (field, error, isValid) => ({ field, error, isValid });
const validationError = (rule, item) => ({ ...rule, item });
const applyRuleSet = ruleSet => itemToValidate =>
$(ruleSet, [fp.map(applyRule(itemToValidate)), fp.filter(isSomething)]);
const applyRule = itemTovalidate => rule =>
rule.isValid(itemTovalidate) ? null : validationError(rule, itemTovalidate);
const compileCode = code => {
let func;
let safeCode;
if (fp.includes("return ")(code)) {
safeCode = code;
} else {
let trimmed = code.trim();
trimmed = trimmed.endsWith(";")
? trimmed.substring(0, trimmed.length - 1)
: trimmed;
safeCode = `return (${trimmed})`;
}
try {
func = compilerUtil.compileCode(safeCode);
} catch (e) {
e.message = `Error compiling code : ${code} : ${e.message}`;
throw e
}
return func
};
const filterEval = "FILTER_EVALUATE";
const filterCompile = "FILTER_COMPILE";
const mapEval = "MAP_EVALUATE";
const mapCompile = "MAP_COMPILE";
const getEvaluateResult = () => ({
isError: false,
passedFilter: true,
result: null,
});
const compileFilter = index => compileCode(index.filter);
const compileMap = index => compileCode(index.map);
const passesFilter = (record, index) => {
const context = { record };
if (!index.filter) return true
const compiledFilter = defineError(() => compileFilter(index), filterCompile);
return defineError(() => compiledFilter(context), filterEval)
};
const mapRecord = (record, index) => {
const recordClone = fp.cloneDeep(record);
const context = { record: recordClone };
const map = index.map ? index.map : "return {...record};";
const compiledMap = defineError(() => compileCode(map), mapCompile);
const mapped = defineError(() => compiledMap(context), mapEval);
const mappedKeys = fp.keys(mapped);
for (let i = 0; i < mappedKeys.length; i++) {
const key = mappedKeys[i];
mapped[key] = fp.isUndefined(mapped[key]) ? null : mapped[key];
if (fp.isFunction(mapped[key])) {
delete mapped[key];
}
if (key === "IsNew") {
delete mapped.IsNew;
}
}
mapped.key = record.key;
mapped.sortKey = index.getSortKey
? compileCode(index.getSortKey)(context)
: record.id;
return mapped
};
const evaluate = record => index => {
const result = getEvaluateResult();
try {
result.passedFilter = passesFilter(record, index);
} catch (err) {
result.isError = true;
result.passedFilter = false;
result.result = err.message;
}
if (!result.passedFilter) return result
try {
result.result = mapRecord(record, index);
} catch (err) {
result.isError = true;
result.result = err.message;
}
return result
};
const indexTypes = { reference: "reference", ancestor: "ancestor" };
const indexRuleSet = [
makerule("map", "index has no map function", index =>
isNonEmptyString(index.map)
),
makerule(
"map",
"index's map function does not compile",
index =>
!isNonEmptyString(index.map) ||
executesWithoutException(() => compileMap(index))
),
makerule(
"filter",
"index's filter function does not compile",
index =>
!isNonEmptyString(index.filter) ||
executesWithoutException(() => compileFilter(index))
),
makerule("name", "must declare a name for index", index =>
isNonEmptyString(index.name)
),
makerule(
"name",
"there is a duplicate named index on this node",
index =>
fp.isEmpty(index.name) ||
fp.countBy("name")(index.parent().indexes)[index.name] === 1
),
makerule(
"indexType",
"reference index may only exist on a record node",
index =>
isRecord(index.parent()) || index.indexType !== indexTypes.reference
),
makerule(
"indexType",
`index type must be one of: ${fp.join(", ")(fp.keys(indexTypes))}`,
index => fp.includes(index.indexType)(fp.keys(indexTypes))
),
];
const getFlattenedHierarchy = (appHierarchy, useCached = true) => {
if (isSomething(appHierarchy.getFlattenedHierarchy) && useCached) {
return appHierarchy.getFlattenedHierarchy()
}
const flattenHierarchy = (currentNode, flattened) => {
flattened.push(currentNode);
if (
(!currentNode.children || currentNode.children.length === 0) &&
(!currentNode.indexes || currentNode.indexes.length === 0) &&
(!currentNode.aggregateGroups || currentNode.aggregateGroups.length === 0)
) {
return flattened
}
const unionIfAny = l2 => l1 => fp.union(l1)(!l2 ? [] : l2);
const children = $(
[],
[
unionIfAny(currentNode.children),
unionIfAny(currentNode.indexes),
unionIfAny(currentNode.aggregateGroups),
]
);
for (const child of children) {
flattenHierarchy(child, flattened);
}
return flattened
};
appHierarchy.getFlattenedHierarchy = () => flattenHierarchy(appHierarchy, []);
return appHierarchy.getFlattenedHierarchy()
};
const getLastPartInKey = key => fp.last(splitKey(key));
const getNodesInPath = appHierarchy => key =>
$(appHierarchy, [
getFlattenedHierarchy,
fp.filter(n => new RegExp(`${n.pathRegx()}`).test(key)),
]);
const getExactNodeForKey = appHierarchy => key =>
$(appHierarchy, [
getFlattenedHierarchy,
fp.find(n => new RegExp(`${n.pathRegx()}$`).test(key)),
]);
const getNodeForCollectionPath = appHierarchy => collectionKey =>
$(appHierarchy, [
getFlattenedHierarchy,
fp.find(
n =>
isCollectionRecord(n) &&
new RegExp(`${n.collectionPathRegx()}$`).test(collectionKey)
),
]);
const hasMatchingAncestor = ancestorPredicate => decendantNode =>
switchCase(
[node => isNothing(node.parent()), fp.constant(false)],
[node => ancestorPredicate(node.parent()), fp.constant(true)],
[defaultCase, node => hasMatchingAncestor(ancestorPredicate)(node.parent())]
)(decendantNode);
const getNode = (appHierarchy, nodeKey) =>
$(appHierarchy, [
getFlattenedHierarchy,
fp.find(
n =>
n.nodeKey() === nodeKey ||
(isCollectionRecord(n) && n.collectionNodeKey() === nodeKey)
),
]);
const getCollectionNode = (appHierarchy, nodeKey) =>
$(appHierarchy, [
getFlattenedHierarchy,
fp.find(n => isCollectionRecord(n) && n.collectionNodeKey() === nodeKey),
]);
const getNodeByKeyOrNodeKey = (appHierarchy, keyOrNodeKey) => {
const nodeByKey = getExactNodeForKey(appHierarchy)(keyOrNodeKey);
return isNothing(nodeByKey) ? getNode(appHierarchy, keyOrNodeKey) : nodeByKey
};
const getCollectionNodeByKeyOrNodeKey = (appHierarchy, keyOrNodeKey) => {
const nodeByKey = getNodeForCollectionPath(appHierarchy)(keyOrNodeKey);
return isNothing(nodeByKey)
? getCollectionNode(appHierarchy, keyOrNodeKey)
: nodeByKey
};
const isNode = (appHierarchy, key) =>
isSomething(getExactNodeForKey(appHierarchy)(key));
const getActualKeyOfParent = (parentNodeKey, actualChildKey) =>
$(actualChildKey, [
splitKey,
fp.take(splitKey(parentNodeKey).length),
ks => joinKey(...ks),
]);
const getParentKey = key => {
return $(key, [splitKey, fp.take(splitKey(key).length - 1), joinKey])
};
const isKeyAncestorOf = ancestorKey => decendantNode =>
hasMatchingAncestor(p => p.nodeKey() === ancestorKey)(decendantNode);
const hasNoMatchingAncestors = parentPredicate => node =>
!hasMatchingAncestor(parentPredicate)(node);
const findField = (recordNode, fieldName) =>
fp.find(f => f.name == fieldName)(recordNode.fields);
const isAncestor = decendant => ancestor =>
isKeyAncestorOf(ancestor.nodeKey())(decendant);
const isDecendant = ancestor => decendant =>
isAncestor(decendant)(ancestor);
const getRecordNodeId = recordKey =>
$(recordKey, [splitKey, fp.last, getRecordNodeIdFromId]);
const getRecordNodeIdFromId = recordId =>
$(recordId, [fp.split("-"), fp.first, parseInt]);
const getRecordNodeById = (hierarchy, recordId) =>
$(hierarchy, [
getFlattenedHierarchy,
fp.find(n => isRecord(n) && n.nodeId === getRecordNodeIdFromId(recordId)),
]);
const recordNodeIdIsAllowed = indexNode => nodeId =>
indexNode.allowedRecordNodeIds.length === 0 ||
fp.includes(nodeId)(indexNode.allowedRecordNodeIds);
const recordNodeIsAllowed = indexNode => recordNode =>
recordNodeIdIsAllowed(indexNode)(recordNode.nodeId);
const getAllowedRecordNodesForIndex = (appHierarchy, indexNode) => {
const recordNodes = $(appHierarchy, [getFlattenedHierarchy, fp.filter(isRecord)]);
if (isGlobalIndex(indexNode)) {
return $(recordNodes, [fp.filter(recordNodeIsAllowed(indexNode))])
}
if (isAncestorIndex(indexNode)) {
return $(recordNodes, [
fp.filter(isDecendant(indexNode.parent())),
fp.filter(recordNodeIsAllowed(indexNode)),
])
}
if (isReferenceIndex(indexNode)) {
return $(recordNodes, [
fp.filter(n => fp.some(fieldReversesReferenceToIndex(indexNode))(n.fields)),
])
}
};
const getDependantIndexes = (hierarchy, recordNode) => {
const allIndexes = $(hierarchy, [getFlattenedHierarchy, fp.filter(isIndex)]);
const allowedAncestors = $(allIndexes, [
fp.filter(isAncestorIndex),
fp.filter(i => recordNodeIsAllowed(i)(recordNode)),
]);
const allowedReference = $(allIndexes, [
fp.filter(isReferenceIndex),
fp.filter(i => fp.some(fieldReversesReferenceToIndex(i))(recordNode.fields)),
]);
return [...allowedAncestors, ...allowedReference]
};
const getNodeFromNodeKeyHash = hierarchy => hash =>
$(hierarchy, [
getFlattenedHierarchy,
fp.find(n => getHashCode(n.nodeKey()) === hash),
]);
const isRecord = node => isSomething(node) && node.type === "record";
const isSingleRecord = node => isRecord(node) && node.isSingle;
const isCollectionRecord = node => isRecord(node) && !node.isSingle;
const isIndex = node => isSomething(node) && node.type === "index";
const isaggregateGroup = node =>
isSomething(node) && node.type === "aggregateGroup";
const isShardedIndex = node =>
isIndex(node) && isNonEmptyString(node.getShardName);
const isRoot = node => isSomething(node) && node.isRoot();
const findRoot = node => (isRoot(node) ? node : findRoot(node.parent()));
const isDecendantOfARecord = hasMatchingAncestor(isRecord);
const isGlobalIndex = node => isIndex(node) && isRoot(node.parent());
const isReferenceIndex = node =>
isIndex(node) && node.indexType === indexTypes.reference;
const isAncestorIndex = node =>
isIndex(node) && node.indexType === indexTypes.ancestor;
const isTopLevelRecord = node => isRoot(node.parent()) && isRecord(node);
const isTopLevelIndex = node => isRoot(node.parent()) && isIndex(node);
const fieldReversesReferenceToNode = node => field =>
field.type === "reference" &&
fp.intersection(field.typeOptions.reverseIndexNodeKeys)(
fp.map(i => i.nodeKey())(node.indexes)
).length > 0;
const fieldReversesReferenceToIndex = indexNode => field =>
field.type === "reference" &&
fp.intersection(field.typeOptions.reverseIndexNodeKeys)([indexNode.nodeKey()])
.length > 0;
const nodeNameFromNodeKey = (hierarchy, nodeKey) => {
const node = getNode(hierarchy, nodeKey);
return node ? node.nodeName() : ""
};
var hierarchy = {
getLastPartInKey,
getNodesInPath,
getExactNodeForKey,
hasMatchingAncestor,
getNode,
getNodeByKeyOrNodeKey,
isNode,
getActualKeyOfParent,
getParentKey,
isKeyAncestorOf,
hasNoMatchingAncestors,
findField,
isAncestor,
isDecendant,
getRecordNodeId,
getRecordNodeIdFromId,
getRecordNodeById,
recordNodeIdIsAllowed,
recordNodeIsAllowed,
getAllowedRecordNodesForIndex,
getNodeFromNodeKeyHash,
isRecord,
isCollectionRecord,
isIndex,
isaggregateGroup,
isShardedIndex,
isRoot,
isDecendantOfARecord,
isGlobalIndex,
isReferenceIndex,
isAncestorIndex,
fieldReversesReferenceToNode,
fieldReversesReferenceToIndex,
getFlattenedHierarchy,
isTopLevelIndex,
isTopLevelRecord,
nodeNameFromNodeKey,
};
const getSafeFieldParser = (tryParse, defaultValueFunctions) => (
field,
record
) => {
if (fp.has(field.name)(record)) {
return getSafeValueParser(
tryParse,
defaultValueFunctions
)(record[field.name])
}
return defaultValueFunctions[field.getUndefinedValue]()
};
const getSafeValueParser = (
tryParse,
defaultValueFunctions
) => value => {
const parsed = tryParse(value);
if (parsed.success) {
return parsed.value
}
return defaultValueFunctions.default()
};
const getNewValue = (tryParse, defaultValueFunctions) => field => {
const getInitialValue =
fp.isUndefined(field) || fp.isUndefined(field.getInitialValue)
? "default"
: field.getInitialValue;
return fp.has(getInitialValue)(defaultValueFunctions)
? defaultValueFunctions[getInitialValue]()
: getSafeValueParser(tryParse, defaultValueFunctions)(getInitialValue)
};
const typeFunctions = specificFunctions =>
_.merge(
{
value: fp.constant,
null: fp.constant(null),
},
specificFunctions
);
const validateTypeConstraints = validationRules => async (
field,
record,
context
) => {
const fieldValue = record[field.name];
const validateRule = async r =>
!(await r.isValid(fieldValue, field.typeOptions, context))
? r.getMessage(fieldValue, field.typeOptions)
: "";
const errors = [];
for (const r of validationRules) {
const err = await validateRule(r);
if (isNotEmpty(err)) errors.push(err);
}
return errors
};
const getDefaultOptions = fp.mapValues(v => v.defaultValue);
const makerule$1 = (isValid, getMessage) => ({ isValid, getMessage });
const parsedFailed = val => ({ success: false, value: val });
const parsedSuccess = val => ({ success: true, value: val });
const getDefaultExport = (
name,
tryParse,
functions,
options,
validationRules,
sampleValue,
stringify
) => ({
getNew: getNewValue(tryParse, functions),
safeParseField: getSafeFieldParser(tryParse, functions),
safeParseValue: getSafeValueParser(tryParse, functions),
tryParse,
name,
getDefaultOptions: () => getDefaultOptions(fp.cloneDeep(options)),
optionDefinitions: options,
validateTypeConstraints: validateTypeConstraints(validationRules),
sampleValue,
stringify: val => (val === null || val === undefined ? "" : stringify(val)),
getDefaultValue: functions.default,
});
const stringFunctions = typeFunctions({
default: fp.constant(null),
});
const stringTryParse = switchCase(
[fp.isString, parsedSuccess],
[fp.isNull, parsedSuccess],
[defaultCase, v => parsedSuccess(v.toString())]
);
const options = {
maxLength: {
defaultValue: null,
isValid: n => n === null || (isSafeInteger(n) && n > 0),
requirementDescription:
"max length must be null (no limit) or a greater than zero integer",
parse: toNumberOrNull,
},
values: {
defaultValue: null,
isValid: v =>
v === null || (isArrayOfString(v) && v.length > 0 && v.length < 10000),
requirementDescription:
"'values' must be null (no values) or an array of at least one string",
parse: s => s,
},
allowDeclaredValuesOnly: {
defaultValue: false,
isValid: fp.isBoolean,
requirementDescription: "allowDeclaredValuesOnly must be true or false",
parse: toBoolOrNull,
},
};
const typeConstraints = [
makerule$1(
async (val, opts) =>
val === null || opts.maxLength === null || val.length <= opts.maxLength,
(val, opts) => `value exceeds maximum length of ${opts.maxLength}`
),
makerule$1(
async (val, opts) =>
val === null ||
opts.allowDeclaredValuesOnly === false ||
fp.includes(val)(opts.values),
val => `"${val}" does not exist in the list of allowed values`
),
];
var string = getDefaultExport(
"string",
stringTryParse,
stringFunctions,
options,
typeConstraints,
"abcde",
str => str
);
const boolFunctions = typeFunctions({
default: fp.constant(null),
});
const boolTryParse = switchCase(
[fp.isBoolean, parsedSuccess],
[fp.isNull, parsedSuccess],
[isOneOf("true", "1", "yes", "on"), () => parsedSuccess(true)],
[isOneOf("false", "0", "no", "off"), () => parsedSuccess(false)],
[defaultCase, parsedFailed]
);
const options$1 = {
allowNulls: {
defaultValue: true,
isValid: fp.isBoolean,
requirementDescription: "must be a true or false",
parse: toBoolOrNull,
},
};
const typeConstraints$1 = [
makerule$1(
async (val, opts) => opts.allowNulls === true || val !== null,
() => "field cannot be null"
),
];
var bool = getDefaultExport(
"bool",
boolTryParse,
boolFunctions,
options$1,
typeConstraints$1,
true,
JSON.stringify
);
const numberFunctions = typeFunctions({
default: fp.constant(null),
});
const parseStringtoNumberOrNull = s => {
const num = Number(s);
return isNaN(num) ? parsedFailed(s) : parsedSuccess(num)
};
const numberTryParse = switchCase(
[fp.isNumber, parsedSuccess],
[fp.isString, parseStringtoNumberOrNull],
[fp.isNull, parsedSuccess],
[defaultCase, parsedFailed]
);
const options$2 = {
maxValue: {
defaultValue: Number.MAX_SAFE_INTEGER,
isValid: isSafeInteger,
requirementDescription: "must be a valid integer",
parse: toNumberOrNull,
},
minValue: {
defaultValue: 0 - Number.MAX_SAFE_INTEGER,
isValid: isSafeInteger,
requirementDescription: "must be a valid integer",
parse: toNumberOrNull,
},
decimalPlaces: {
defaultValue: 0,
isValid: n => isSafeInteger(n) && n >= 0,
requirementDescription: "must be a positive integer",
parse: toNumberOrNull,
},
};
const getDecimalPlaces = val => {
const splitDecimal = val.toString().split(".");
if (splitDecimal.length === 1) return 0
return splitDecimal[1].length
};
const typeConstraints$2 = [
makerule$1(
async (val, opts) =>
val === null || opts.minValue === null || val >= opts.minValue,
(val, opts) =>
`value (${val.toString()}) must be greater than or equal to ${
opts.minValue
}`
),
makerule$1(
async (val, opts) =>
val === null || opts.maxValue === null || val <= opts.maxValue,
(val, opts) =>
`value (${val.toString()}) must be less than or equal to ${
opts.minValue
} options`
),
makerule$1(
async (val, opts) =>
val === null || opts.decimalPlaces >= getDecimalPlaces(val),
(val, opts) =>
`value (${val.toString()}) must have ${
opts.decimalPlaces
} decimal places or less`
),
];
var number = getDefaultExport(
"number",
numberTryParse,
numberFunctions,
options$2,
typeConstraints$2,
1,
num => num.toString()
);
const dateFunctions = typeFunctions({
default: fp.constant(null),
now: () => new Date(),
});
const isValidDate = d => d instanceof Date && !isNaN(d);
const parseStringToDate = s =>
switchCase(
[isValidDate, parsedSuccess],
[defaultCase, parsedFailed]
)(new Date(s));
const isNullOrEmpty = d =>
fp.isNull(d)
|| (d || "").toString() === "";
const isDateOrEmpty = d =>
fp.isDate(d)
|| isNullOrEmpty(d);
const dateTryParse = switchCase(
[isDateOrEmpty, parsedSuccess],
[fp.isString, parseStringToDate],
[defaultCase, parsedFailed]
);
const options$3 = {
maxValue: {
defaultValue: null,
//defaultValue: new Date(32503680000000),
isValid: isDateOrEmpty,
requirementDescription: "must be a valid date",
parse: toDateOrNull,
},
minValue: {
defaultValue: null,
//defaultValue: new Date(-8520336000000),
isValid: isDateOrEmpty,
requirementDescription: "must be a valid date",
parse: toDateOrNull,
},
};
const typeConstraints$3 = [
makerule$1(
async (val, opts) =>
val === null || isNullOrEmpty(opts.minValue) || val >= opts.minValue,
(val, opts) =>
`value (${val.toString()}) must be greater than or equal to ${
opts.minValue
}`
),
makerule$1(
async (val, opts) =>
val === null || isNullOrEmpty(opts.maxValue) || val <= opts.maxValue,
(val, opts) =>
`value (${val.toString()}) must be less than or equal to ${
opts.minValue
} options`
),
];
var datetime = getDefaultExport(
"datetime",
dateTryParse,
dateFunctions,
options$3,
typeConstraints$3,
new Date(1984, 4, 1),
date => JSON.stringify(date).replace(new RegExp('"', "g"), "")
);
const arrayFunctions = () =>
typeFunctions({
default: fp.constant([]),
});
const mapToParsedArrary = type =>
$$(
fp.map(i => type.safeParseValue(i)),
parsedSuccess
);
const arrayTryParse = type =>
switchCase([fp.isArray, mapToParsedArrary(type)], [defaultCase, parsedFailed]);
const typeName = type => `array<${type}>`;
const options$4 = {
maxLength: {
defaultValue: 10000,
isValid: isSafeInteger,
requirementDescription: "must be a positive integer",
parse: toNumberOrNull,
},
minLength: {
defaultValue: 0,
isValid: n => isSafeInteger(n) && n >= 0,
requirementDescription: "must be a positive integer",
parse: toNumberOrNull,
},
};
const typeConstraints$4 = [
makerule$1(
async (val, opts) => val === null || val.length >= opts.minLength,
(val, opts) => `must choose ${opts.minLength} or more options`
),
makerule$1(
async (val, opts) => val === null || val.length <= opts.maxLength,
(val, opts) => `cannot choose more than ${opts.maxLength} options`
),
];
var array = type =>
getDefaultExport(
typeName(type.name),
arrayTryParse(type),
arrayFunctions(),
options$4,
typeConstraints$4,
[type.sampleValue],
JSON.stringify
);
const referenceNothing = () => ({ key: "" });
const referenceFunctions = typeFunctions({
default: referenceNothing,
});
const hasStringValue = (ob, path) => fp.has(path)(ob) && fp.isString(ob[path]);
const isObjectWithKey = v => fp.isObjectLike(v) && hasStringValue(v, "key");
const tryParseFromString = s => {
try {
const asObj = JSON.parse(s);
if (isObjectWithKey) {
return parsedSuccess(asObj)
}
} catch (_) {
// EMPTY
}
return parsedFailed(s)
};
const referenceTryParse = v =>
switchCase(
[isObjectWithKey, parsedSuccess],
[fp.isString, tryParseFromString],
[fp.isNull, () => parsedSuccess(referenceNothing())],
[defaultCase, parsedFailed]
)(v);
const options$5 = {
indexNodeKey: {
defaultValue: null,
isValid: isNonEmptyString,
requirementDescription: "must be a non-empty string",
parse: s => s,
},
displayValue: {
defaultValue: "",
isValid: isNonEmptyString,
requirementDescription: "must be a non-empty string",
parse: s => s,
},
reverseIndexNodeKeys: {
defaultValue: null,
isValid: v => isArrayOfString(v) && v.length > 0,
requirementDescription: "must be a non-empty array of strings",
parse: s => s,
},
};
const isEmptyString = s => fp.isString(s) && fp.isEmpty(s);
const ensureReferenceExists = async (val, opts, context) =>
isEmptyString(val.key) || (await context.referenceExists(opts, val.key));
const typeConstraints$5 = [
makerule$1(
ensureReferenceExists,
(val, opts) =>
`"${val[opts.displayValue]}" does not exist in options list (key: ${
val.key
})`
),
];
var reference = getDefaultExport(
"reference",
referenceTryParse,
referenceFunctions,
options$5,
typeConstraints$5,
{ key: "key", value: "value" },
JSON.stringify
);
const illegalCharacters = "*?\\/:<>|\0\b\f\v";
const isLegalFilename = filePath => {
const fn = fileName(filePath);
return (
fn.length <= 255 &&
fp.intersection(fn.split(""))(illegalCharacters.split("")).length === 0 &&
none(f => f === "..")(splitKey(filePath))
)
};
const fileNothing = () => ({ relativePath: "", size: 0 });
const fileFunctions = typeFunctions({
default: fileNothing,
});
const fileTryParse = v =>
switchCase(
[isValidFile, parsedSuccess],
[fp.isNull, () => parsedSuccess(fileNothing())],
[defaultCase, parsedFailed]
)(v);
const fileName = filePath => $(filePath, [splitKey, fp.last]);
const isValidFile = f =>
!fp.isNull(f) &&
fp.has("relativePath")(f) &&
fp.has("size")(f) &&
fp.isNumber(f.size) &&
fp.isString(f.relativePath) &&
isLegalFilename(f.relativePath);
const options$6 = {};
const typeConstraints$6 = [];
var file = getDefaultExport(
"file",
fileTryParse,
fileFunctions,
options$6,
typeConstraints$6,
{ relativePath: "some_file.jpg", size: 1000 },
JSON.stringify
);
const allTypes = () => {
const basicTypes = {
string,
number,
datetime,
bool,
reference,
file,
};
const arrays = $(basicTypes, [
fp.keys,
fp.map(k => {
const kvType = {};
const concreteArray = array(basicTypes[k]);
kvType[concreteArray.name] = concreteArray;
return kvType
}),
types => _.assign({}, ...types),
]);
return _.merge({}, basicTypes, arrays)
};
const all$1 = allTypes();
const getType = typeName => {
if (!fp.has(typeName)(all$1))
throw new BadRequestError(`Do not recognise type ${typeName}`)
return all$1[typeName]
};
const getSampleFieldValue = field => getType(field.type).sampleValue;
const getNewFieldValue = field => getType(field.type).getNew(field);
const safeParseField = (field, record) =>
getType(field.type).safeParseField(field, record);
const validateFieldParse = (field, record) =>
fp.has(field.name)(record)
? getType(field.type).tryParse(record[field.name])
: parsedSuccess(undefined); // fields may be undefined by default
const getDefaultOptions$1 = type => getType(type).getDefaultOptions();
const validateTypeConstraints$1 = async (field, record, context) =>
await getType(field.type).validateTypeConstraints(field, record, context);
const detectType = value => {
if (fp.isString(value)) return string
if (fp.isBoolean(value)) return bool
if (fp.isNumber(value)) return number
if (fp.isDate(value)) return datetime
if (fp.isArray(value)) return array(detectType(value[0]))
if (fp.isObject(value) && fp.has("key")(value) && fp.has("value")(value))
return reference
if (fp.isObject(value) && fp.has("relativePath")(value) && fp.has("size")(value))
return file
throw new BadRequestError(`cannot determine type: ${JSON.stringify(value)}`)
};
// 5 minutes
const tempCodeExpiryLength = 5 * 60 * 1000;
const AUTH_FOLDER = "/.auth";
const USERS_LIST_FILE = joinKey(AUTH_FOLDER, "users.json");
const userAuthFile = username =>
joinKey(AUTH_FOLDER, `auth_${username}.json`);
const USERS_LOCK_FILE = joinKey(AUTH_FOLDER, "users_lock");
const ACCESS_LEVELS_FILE = joinKey(AUTH_FOLDER, "access_levels.json");
const ACCESS_LEVELS_LOCK_FILE = joinKey(
AUTH_FOLDER,
"access_levels_lock"
);
const permissionTypes = {
CREATE_RECORD: "create record",
UPDATE_RECORD: "update record",
READ_RECORD: "read record",
DELETE_RECORD: "delete record",
READ_INDEX: "read index",
MANAGE_INDEX: "manage index",
MANAGE_COLLECTION: "manage collection",
WRITE_TEMPLATES: "write templates",
CREATE_USER: "create user",
SET_PASSWORD: "set password",
CREATE_TEMPORARY_ACCESS: "create temporary access",
ENABLE_DISABLE_USER: "enable or disable user",
WRITE_ACCESS_LEVELS: "write access levels",
LIST_USERS: "list users",
LIST_ACCESS_LEVELS: "list access levels",
EXECUTE_ACTION: "execute action",
SET_USER_ACCESS_LEVELS: "set user access levels",
};
const getUserByName = (users, name) =>
$(users, [fp.find(u => u.name.toLowerCase() === name.toLowerCase())]);
const stripUserOfSensitiveStuff = user => {
const stripped = fp.clone(user);
delete stripped.tempCode;
return stripped
};
const parseTemporaryCode = fullCode =>
$(fullCode, [
fp.split(":"),
parts => ({
id: parts[1],
code: parts[2],
}),
]);
const isAuthorized = app => (permissionType, resourceKey) =>
apiWrapperSync(
app,
events.authApi.isAuthorized,
alwaysAuthorized,
{ resourceKey, permissionType },
_isAuthorized,
app,
permissionType,
resourceKey
);
const _isAuthorized = (app, permissionType, resourceKey) => {
if (!app.user) {
return false
}
const validType = $(permissionTypes, [fp.values, fp.includes(permissionType)]);
if (!validType) {
return false
}
const permMatchesResource = userperm => {
const nodeKey = isNothing(resourceKey)
? null
: isNode(app.hierarchy, resourceKey)
? getNodeByKeyOrNodeKey(app.hierarchy, resourceKey).nodeKey()
: resourceKey;
return (
userperm.type === permissionType &&
(isNothing(resourceKey) || nodeKey === userperm.nodeKey)
)
};
return $(app.user.permissions, [fp.some(permMatchesResource)])
};
const nodePermission = type => ({
add: (nodeKey, accessLevel) =>
accessLevel.permissions.push({ type, nodeKey }),
isAuthorized: resourceKey => app => isAuthorized(app)(type, resourceKey),
isNode: true,
get: nodeKey => ({ type, nodeKey }),
});
const staticPermission = type => ({
add: accessLevel => accessLevel.permissions.push({ type }),
isAuthorized: app => isAuthorized(app)(type),
isNode: false,
get: () => ({ type }),
});
const createRecord = nodePermission(permissionTypes.CREATE_RECORD);
const updateRecord = nodePermission(permissionTypes.UPDATE_RECORD);
const deleteRecord = nodePermission(permissionTypes.DELETE_RECORD);
const readRecord = nodePermission(permissionTypes.READ_RECORD);
const writeTemplates = staticPermission(permissionTypes.WRITE_TEMPLATES);
const createUser = staticPermission(permissionTypes.CREATE_USER);
const setPassword = staticPermission(permissionTypes.SET_PASSWORD);
const readIndex = nodePermission(permissionTypes.READ_INDEX);
const manageIndex = staticPermission(permissionTypes.MANAGE_INDEX);
const manageCollection = staticPermission(permissionTypes.MANAGE_COLLECTION);
const createTemporaryAccess = staticPermission(
permissionTypes.CREATE_TEMPORARY_ACCESS
);
const enableDisableUser = staticPermission(permissionTypes.ENABLE_DISABLE_USER);
const writeAccessLevels = staticPermission(permissionTypes.WRITE_ACCESS_LEVELS);
const listUsers = staticPermission(permissionTypes.LIST_USERS);
const listAccessLevels = staticPermission(permissionTypes.LIST_ACCESS_LEVELS);
const setUserAccessLevels = staticPermission(
permissionTypes.SET_USER_ACCESS_LEVELS
);
const executeAction = nodePermission(permissionTypes.EXECUTE_ACTION);
const alwaysAuthorized = () => true;
const permission = {
createRecord,
updateRecord,
deleteRecord,
readRecord,
writeTemplates,
createUser,
setPassword,
readIndex,
createTemporaryAccess,
enableDisableUser,
writeAccessLevels,
listUsers,
listAccessLevels,
manageIndex,
manageCollection,
executeAction,
setUserAccessLevels,
};
const getNew = app => (collectionKey, recordTypeName) => {
const recordNode = getRecordNode(app, collectionKey);
collectionKey = safeKey(collectionKey);
return apiWrapperSync(
app,
events.recordApi.getNew,
permission.createRecord.isAuthorized(recordNode.nodeKey()),
{ collectionKey, recordTypeName },
_getNew,
recordNode,
collectionKey
)
};
/**
* Constructs a record object that can be saved to the backend.
* @param {*} recordNode - record
* @param {*} collectionKey - nested collection key that the record will be saved to.
*/
const _getNew = (recordNode, collectionKey) =>
constructRecord(recordNode, getNewFieldValue, collectionKey);
const getRecordNode = (app, collectionKey) => {
collectionKey = safeKey(collectionKey);
return getNodeForCollectionPath(app.hierarchy)(collectionKey)
};
const getNewChild = app => (recordKey, collectionName, recordTypeName) =>
getNew(app)(joinKey(recordKey, collectionName), recordTypeName);
const constructRecord = (recordNode, getFieldValue, collectionKey) => {
const record = $(recordNode.fields, [fp.keyBy("name"), fp.mapValues(getFieldValue)]);
record.id = `${recordNode.nodeId}-${shortid.generate()}`;
record.key = isSingleRecord(recordNode)
? joinKey(collectionKey, recordNode.name)
: joinKey(collectionKey, record.id);
record.isNew = true;
record.type = recordNode.name;
return record
};
const allIdChars =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
// this should never be changed - ever
// - existing databases depend on the order of chars this string
/**
* folderStructureArray should return an array like
* - [1] = all records fit into one folder
* - [2] = all records fite into 2 folders
* - [64, 3] = all records fit into 64 * 3 folders
* - [64, 64, 10] = all records fit into 64 * 64 * 10 folder
* (there are 64 possible chars in allIsChars)
*/
const folderStructureArray = recordNode => {
const totalFolders = Math.ceil(recordNode.estimatedRecordCount / 1000);
const folderArray = [];
let levelCount = 1;
while (64 ** levelCount < totalFolders) {
levelCount += 1;
folderArray.push(64);
}
const parentFactor = 64 ** folderArray.length;
if (parentFactor < totalFolders) {
folderArray.push(Math.ceil(totalFolders / parentFactor));
}
return folderArray
/*
const maxRecords = currentFolderPosition === 0
? RECORDS_PER_FOLDER
: currentFolderPosition * 64 * RECORDS_PER_FOLDER;
if(maxRecords < recordNode.estimatedRecordCount) {
return folderStructureArray(
recordNode,
[...currentArray, 64],
currentFolderPosition + 1);
} else {
const childFolderCount = Math.ceil(recordNode.estimatedRecordCount / maxRecords );
return [...currentArray, childFolderCount]
}*/
};
const getAllIdsIterator = app => async collection_Key_or_NodeKey => {
collection_Key_or_NodeKey = safeKey(collection_Key_or_NodeKey);
const recordNode =
getCollectionNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey) ||
getNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey);
const getAllIdsIteratorForCollectionKey = async (
recordNode,
collectionKey
) => {
const folderStructure = folderStructureArray(recordNode);
let currentFolderContents = [];
let currentPosition = [];
const collectionDir = getCollectionDir(app.hierarchy, collectionKey);
const basePath = joinKey(collectionDir, recordNode.nodeId.toString());
// "folderStructure" determines the top, sharding folders
// we need to add one, for the collection root folder, which
// always exists
const levels = folderStructure.length + 1;
const topLevel = levels - 1;
/* populate initial directory structure in form:
[
{path: "/a", contents: ["b", "c", "d"]},
{path: "/a/b", contents: ["e","f","g"]},
{path: "/a/b/e", contents: ["1-abcd","2-cdef","3-efgh"]},
]
// stores contents on each parent level
// top level has ID folders
*/
const firstFolder = async () => {
let folderLevel = 0;
const lastPathHasContent = () =>
folderLevel === 0 ||
currentFolderContents[folderLevel - 1].contents.length > 0;
while (folderLevel <= topLevel && lastPathHasContent()) {
let thisPath = basePath;
for (let lev = 0; lev < currentPosition.length; lev++) {
thisPath = joinKey(thisPath, currentFolderContents[lev].contents[0]);
}
con