import { each, find } from 'lodash'; import { map, max, constant } from 'lodash/fp'; import { switchCase, defaultCase, joinKey, $, isNothing, isSomething, } from '../common'; import { isIndex, isRoot, isSingleRecord, isCollectionRecord, isRecord, isaggregateGroup, getFlattenedHierarchy, } from './hierarchy'; import { all } from '../types'; import { BadRequestError } from '../common/errors'; export const createNodeErrors = { indexCannotBeParent: 'Index template cannot be a parent', allNonRootNodesMustHaveParent: 'Only the root node may have no parent', indexParentMustBeRecordOrRoot: 'An index may only have a record or root as a parent', aggregateParentMustBeAnIndex: 'aggregateGroup parent must be an index', }; const pathRegxMaker = node => () => node.nodeKey().replace(/{id}/g, '[a-zA-Z0-9_-]+'); const nodeKeyMaker = node => () => switchCase( [n => isRecord(n) && !isSingleRecord(n), n => joinKey( node.parent().nodeKey(), node.collectionName, `${n.nodeId}-{id}`, )], [isRoot, constant('/')], [defaultCase, n => joinKey(node.parent().nodeKey(), n.name)], )(node); const validate = parent => (node) => { if (isIndex(node) && isSomething(parent) && !isRoot(parent) && !isRecord(parent)) { throw new BadRequestError(createNodeErrors.indexParentMustBeRecordOrRoot); } if (isaggregateGroup(node) && isSomething(parent) && !isIndex(parent)) { throw new BadRequestError(createNodeErrors.aggregateParentMustBeAnIndex); } if (isNothing(parent) && !isRoot(node)) { throw new BadRequestError(createNodeErrors.allNonRootNodesMustHaveParent); } return node; }; const construct = parent => (node) => { node.nodeKey = nodeKeyMaker(node); node.pathRegx = pathRegxMaker(node); node.parent = constant(parent); node.isRoot = () => isNothing(parent) && node.name === 'root' && node.type === 'root'; if (isCollectionRecord(node)) { node.collectionNodeKey = () => joinKey( parent.nodeKey(), node.collectionName, ); node.collectionPathRegx = () => joinKey( parent.pathRegx(), node.collectionName, ); } return node; }; const addToParent = (obj) => { const parent = obj.parent(); if (isSomething(parent)) { if (isIndex(obj)) // Q: why are indexes not children ? // A: because they cannot have children of their own. { parent.indexes.push(obj); } else if (isaggregateGroup(obj)) { parent.aggregateGroups.push(obj); } else { parent.children.push(obj); } if (isRecord(obj)) { const defaultIndex = find( parent.indexes, i => i.name === `${parent.name}_index`, ); if (defaultIndex) { defaultIndex.allowedRecordNodeIds.push(obj.nodeId); } } } return obj; }; export const constructNode = (parent, obj) => $(obj, [ construct(parent), validate(parent), addToParent, ]); const getNodeId = (parentNode) => { // this case is handled better elsewhere if (!parentNode) return null; const findRoot = n => (isRoot(n) ? n : findRoot(n.parent())); const root = findRoot(parentNode); return ($(root, [ getFlattenedHierarchy, map(n => n.nodeId), max]) + 1); }; export const constructHierarchy = (node, parent) => { construct(parent)(node); if (node.indexes) { each(node.indexes, child => constructHierarchy(child, node)); } if (node.aggregateGroups) { each(node.aggregateGroups, child => constructHierarchy(child, node)); } if (node.children && node.children.length > 0) { each(node.children, child => constructHierarchy(child, node)); } if (node.fields) { each(node.fields, f => each(f.typeOptions, (val, key) => { const def = all[f.type].optionDefinitions[key]; if (!def) { // unknown typeOption delete f.typeOptions[key]; } else { f.typeOptions[key] = def.parse(val); } })); } return node; }; export const getNewRootLevel = () => construct()({ name: 'root', type: 'root', children: [], pathMaps: [], indexes: [], nodeId: 0, }); const _getNewRecordTemplate = (parent, name, createDefaultIndex, isSingle) => { const node = constructNode(parent, { name, type: 'record', fields: [], children: [], validationRules: [], nodeId: getNodeId(parent), indexes: [], allidsShardFactor: isRecord(parent) ? 1 : 64, collectionName: '', isSingle, }); if (createDefaultIndex) { const defaultIndex = getNewIndexTemplate(parent); defaultIndex.name = `${name}_index`; defaultIndex.allowedRecordNodeIds.push(node.nodeId); } return node; }; export const getNewRecordTemplate = (parent, name = '', createDefaultIndex = true) => _getNewRecordTemplate(parent, name, createDefaultIndex, false); export const getNewSingleRecordTemplate = parent => _getNewRecordTemplate(parent, '', false, true); export const getNewIndexTemplate = (parent, type = 'ancestor') => constructNode(parent, { name: '', type: 'index', map: 'return {...record};', filter: '', indexType: type, getShardName: '', getSortKey: 'record.id', aggregateGroups: [], allowedRecordNodeIds: [], nodeId: getNodeId(parent), }); export const getNewAggregateGroupTemplate = index => constructNode(index, { name: '', type: 'aggregateGroup', groupBy: '', aggregates: [], condition: '', nodeId: getNodeId(index), }); export const getNewAggregateTemplate = (set) => { const aggregatedValue = { name: '', aggregatedValue: '', }; set.aggregates.push(aggregatedValue); return aggregatedValue; }; export default { getNewRootLevel, getNewRecordTemplate, getNewIndexTemplate, createNodeErrors, constructHierarchy, getNewAggregateGroupTemplate, getNewAggregateTemplate, };