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 nodeNameMaker = node => () => isRoot(node) ? "/" : joinKey(node.parent().nodeName(), node.name) 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.nodeName = nodeNameMaker(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 _getNewModelTemplate = (parent, name, createDefaultIndex, isSingle) => { const nodeId = getNodeId(parent) const node = constructNode(parent, { name, type: "record", fields: [], children: [], validationRules: [], nodeId: nodeId, indexes: [], estimatedRecordCount: isRecord(parent) ? 500 : 1000000, collectionName: (nodeId || "").toString(), isSingle, }) if (createDefaultIndex) { const defaultIndex = getNewIndexTemplate(parent) defaultIndex.name = `${name}_index` defaultIndex.allowedRecordNodeIds.push(node.nodeId) } return node } export const getNewModelTemplate = ( parent, name = "", createDefaultIndex = true ) => _getNewModelTemplate(parent, name, createDefaultIndex, false) export const getNewSingleRecordTemplate = parent => _getNewModelTemplate(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, getNewModelTemplate, getNewIndexTemplate, createNodeErrors, constructHierarchy, getNewAggregateGroupTemplate, getNewAggregateTemplate, }