UNPKG

@paroicms/server

Version:
166 lines 7.48 kB
import { getDocumentTypeByName, getNodeTypeByName, isRootOfSubRoutingCluster, } from "@paroicms/internal-anywhere-lib"; import { ApiError } from "@paroicms/public-server-lib"; import { type } from "arktype"; import { invalidateNodeInCache } from "../../common/text-cache.js"; import { findAvailableRelativeId } from "../../connector/db-init/init-node-queries.js"; import { countNodeChildrenOf, getLanguagesOfNode } from "../node/node.queries.js"; export async function moveDocument(siteContext, { documentNodeId, newParentNodeId, }) { const docInfo = await getDocumentInfoForMove(siteContext, documentNodeId); validateNotSubRoutingClusterRoot(siteContext, docInfo.typeName); const parentInfo = await getNewParentInfo(siteContext, newParentNodeId); validateTypeCompatibility(siteContext, docInfo.typeName, parentInfo.typeName); await validateChildLimit(siteContext, newParentNodeId, parentInfo.typeName); await validateLanguageCompatibility(siteContext, documentNodeId, newParentNodeId); await validateNoCircularReference(siteContext, documentNodeId, newParentNodeId); const newRelativeId = await checkRelativeIdCollision(siteContext, documentNodeId, newParentNodeId, docInfo.relativeId); await executeMoveDocument(siteContext, documentNodeId, docInfo.parentId, newParentNodeId, parentInfo.typeName, newRelativeId); } const DocumentInfoForMoveAT = type({ typeName: "string", parentId: "number|null", relativeId: "string", "+": "reject", }).pipe((r) => ({ typeName: r.typeName, parentId: r.parentId === null ? undefined : String(r.parentId), relativeId: r.relativeId, })); async function getDocumentInfoForMove(siteContext, documentNodeId) { const { cn } = siteContext; const row = await cn("PaNode as n") .select("n.typeName", "n.parentId", "n.relativeId") .where("n.id", documentNodeId) .first(); if (!row) throw new ApiError(`cannot find document node '${documentNodeId}'`, 404); return DocumentInfoForMoveAT.assert(row); } function validateNotSubRoutingClusterRoot(siteContext, typeName) { const nodeType = getNodeTypeByName(siteContext.siteSchema, typeName); if (nodeType.kind !== "document" || nodeType.documentKind !== "regular") { throw new ApiError("only regular documents can be moved", 400); } if (isRootOfSubRoutingCluster(nodeType)) { throw new ApiError("cannot move a sub-routing-cluster root document", 400); } } const NewParentInfoAT = type({ typeName: "string", "+": "reject", }); async function getNewParentInfo(siteContext, newParentNodeId) { const { cn } = siteContext; const row = await cn("PaNode as n") .select("n.typeName") .where("n.id", newParentNodeId) .first(); if (!row) throw new ApiError(`cannot find new parent node '${newParentNodeId}'`, 404); return NewParentInfoAT.assert(row); } function validateTypeCompatibility(siteContext, documentTypeName, parentTypeName) { const parentDocType = getDocumentTypeByName(siteContext.siteSchema, parentTypeName); if (!parentDocType.regularChildren?.includes(documentTypeName)) { throw new ApiError(`document type '${documentTypeName}' is not a valid regular child of '${parentTypeName}'`, 400); } } async function validateChildLimit(siteContext, newParentNodeId, parentTypeName) { const parentDocType = getDocumentTypeByName(siteContext.siteSchema, parentTypeName); if (parentDocType.childLimit !== undefined) { const count = await countNodeChildrenOf(siteContext, newParentNodeId); if (count >= parentDocType.childLimit) { throw new ApiError("maximum children reached on new parent", 400); } } } async function validateLanguageCompatibility(siteContext, documentNodeId, newParentNodeId) { const docLanguages = await getLanguagesOfNode(siteContext, documentNodeId); const parentLanguages = await getLanguagesOfNode(siteContext, newParentNodeId); const parentLangSet = new Set(parentLanguages); for (const lang of docLanguages) { if (!parentLangSet.has(lang)) { throw new ApiError(`document has language '${lang}' which does not exist on the new parent`, 400); } } } const AncestorRowAT = type({ parentId: "number|null", "+": "reject", }); async function validateNoCircularReference(siteContext, documentNodeId, newParentNodeId) { const { cn } = siteContext; let currentId = newParentNodeId; while (currentId) { if (currentId === documentNodeId) { throw new ApiError("cannot move document into its own descendant", 400); } const row = await cn("PaNode as n") .select("n.parentId") .where("n.id", currentId) .first(); if (!row) break; const validated = AncestorRowAT.assert(row); currentId = validated.parentId === null ? undefined : String(validated.parentId); } } async function checkRelativeIdCollision(siteContext, documentNodeId, newParentNodeId, currentRelativeId) { const { cn, siteSchema } = siteContext; const existing = await cn("PaNode as n") .select("n.id") .where("n.parentId", newParentNodeId) .andWhere("n.relativeId", currentRelativeId) .andWhereNot("n.id", documentNodeId) .first(); if (!existing) return undefined; const docInfo = await getDocumentInfoForMove(siteContext, documentNodeId); return await findAvailableRelativeId(siteSchema, cn, { parentId: newParentNodeId, typeName: docInfo.typeName, }); } async function executeMoveDocument(siteContext, documentNodeId, oldParentNodeId, newParentNodeId, newParentTypeName, newRelativeId) { const { cn, siteSchema } = siteContext; const parentDocType = getDocumentTypeByName(siteSchema, newParentTypeName); await cn.transaction(async (tx) => { const updateData = { parentId: newParentNodeId }; if (newRelativeId) { updateData.relativeId = newRelativeId; } await tx("PaNode").where("id", documentNodeId).update(updateData); if (parentDocType.regularChildrenSorting === "manual") { await updateDocumentOrdering(tx, documentNodeId, newParentNodeId); } }); if (oldParentNodeId) { await invalidateNodeInCache(siteContext, { nodeId: oldParentNodeId }); } await invalidateNodeInCache(siteContext, { nodeId: newParentNodeId }); await invalidateNodeInCache(siteContext, { nodeId: documentNodeId }); } async function updateDocumentOrdering(tx, documentNodeId, newParentNodeId) { const orderRow = await tx("PaNode as n") .select(tx.raw("max(o.orderNum) as orderNum")) .innerJoin("PaOrderedNode as o", "o.nodeId", "n.id") .where("n.parentId", newParentNodeId) .andWhereNot("n.id", documentNodeId) .first(); const maxOrderNum = Number(orderRow?.orderNum) || 0; const existingOrder = await tx("PaOrderedNode") .select("nodeId") .where("nodeId", documentNodeId) .first(); if (existingOrder) { await tx("PaOrderedNode") .where("nodeId", documentNodeId) .update({ orderNum: maxOrderNum + 1 }); } else { await tx("PaOrderedNode").insert({ nodeId: documentNodeId, orderNum: maxOrderNum + 1, }); } } //# sourceMappingURL=document-moving.queries.js.map