UNPKG

@paroicms/server

Version:
228 lines 9.89 kB
import { documentTypeHasData, getDocumentTypeByName, isRootOfRoutingCluster, } from "@paroicms/internal-anywhere-lib"; import { isDef, } from "@paroicms/public-anywhere-lib"; import { ApiError, createSimpleTranslator, } from "@paroicms/public-server-lib"; import { type } from "arktype"; import { createNode } from "../../connector/db-init/init-node-queries.js"; import { reloadHomeRoutingCluster } from "../../site-context/refresh-site-context.js"; import { getLanguagesOfNode, getTypeNameOf } from "../node/node.queries.js"; import { haveRegularChildren } from "./cluster-validation.js"; const IdWithTypeNameRowAT = type({ id: "number", typeName: "string", "+": "reject", }).pipe((r) => ({ id: String(r.id), typeName: r.typeName, })); export async function manageClusterRoutingDocuments(siteContext, request) { const { cn, siteSchema, logger } = siteContext; const schemaI18n = createSimpleTranslator({ labels: siteSchema.l10n, logger }); const { children } = request; if (!children || children.length === 0) return; const rootTypeName = await getTypeNameOf(siteContext, request.rootNodeId); const rootType = getDocumentTypeByName(siteSchema, rootTypeName); if (rootType.typeName !== request.rootTypeName) { throw new ApiError(`Root type name mismatch for node '${request.rootNodeId}': expected '${rootType.typeName}', got '${request.rootTypeName}'`, 400); } if (!isRootOfRoutingCluster(rootType)) { throw new ApiError(`Node '${request.rootNodeId}' is not a valid root of a routing cluster`, 400); } const languages = await getLanguagesOfNode(siteContext, request.rootNodeId); await cn.transaction(async (tx) => { await processClusterNode(siteContext, tx, schemaI18n, { parentType: rootType, parentNodeId: request.rootNodeId, nodes: children, languages, }); }); if (rootTypeName === "home") { await reloadHomeRoutingCluster(siteContext.fqdn); } } async function processClusterNode(siteContext, tx, schemaI18n, { parentType, parentNodeId, nodes, languages, }) { const { siteSchema } = siteContext; for (const node of nodes) { if (!parentType?.routingChildren?.includes(node.typeName)) { throw new ApiError(`Invalid routing child '${node.typeName}' of '${parentType.typeName}'`, 400); } const documentType = getDocumentTypeByName(siteSchema, node.typeName); if (node.action === "insert") { if (documentType.documentKind !== "routing") throw new Error("should be a routing document"); const newNode = await createRoutingLNode(tx, siteContext, schemaI18n, { parentNodeId, node, languages, documentType, }); if (node.children && node.children.length > 0) { await processClusterNode(siteContext, tx, schemaI18n, { parentType: documentType, parentNodeId: newNode.id, nodes: node.children, languages, }); } } else if (node.action === "delete") { if (!isDef(node.nodeId)) { throw new ApiError(`Node ID is required for deletion of '${node.typeName}'`, 400); } if (await haveRegularChildren(tx, siteContext.siteSchema, { nodeIds: [node.nodeId] })) { throw new Error(`Cannot delete routing document ${node.typeName}: it has children`); } await tx("PaDocument").where("nodeId", node.nodeId).del(); await tx("PaLNode").where("nodeId", node.nodeId).del(); await tx("PaNode").where("id", node.nodeId).del(); } else if (isDef(node.nodeId) && node.children && node.children.length > 0) { await processClusterNode(siteContext, tx, schemaI18n, { parentType: documentType, parentNodeId: node.nodeId, nodes: node.children, languages, }); } } } async function createRoutingLNode(tx, siteContext, schemaI18n, { parentNodeId, node, languages, documentType, }) { const { siteSchema } = siteContext; const newNode = await createNode(siteSchema, tx, { parentId: parentNodeId, typeName: node.typeName, }); const makeTitle = (language) => schemaI18n.translate({ key: `nodeTypes.${node.typeName}.label`, language, defaultValue: node.typeName, }); for (const language of languages) { await tx("PaLNode").insert({ language, nodeId: newNode.id, ready: true, updatedAt: tx.raw("current_timestamp"), }); if (documentTypeHasData(documentType)) { await tx("PaDocument").insert({ language, nodeId: newNode.id, title: makeTitle(language), }); } } return newNode; } export async function deleteCluster(siteContext, rootNodeId, onlyLanguages) { const { cn, siteSchema } = siteContext; const rootTypeName = await getTypeNameOf(siteContext, rootNodeId); const rootType = getDocumentTypeByName(siteSchema, rootTypeName); if (!isRootOfRoutingCluster(rootType)) { throw new ApiError(`Node '${rootNodeId}' is not a root of a routing cluster`, 400); } const clusterLanguages = await getLanguagesOfNode(siteContext, rootNodeId); const languages = !onlyLanguages || (onlyLanguages.length >= clusterLanguages.length && clusterLanguages.every((lang) => onlyLanguages.includes(lang))) ? undefined : onlyLanguages; const clusterNodeIds = await getAllNodeIdsOfRoutingCluster(cn, siteSchema, rootNodeId, rootType); if (await haveRegularChildren(cn, siteSchema, { nodeIds: clusterNodeIds, languages })) { throw new ApiError(`Cannot delete cluster in [${languages ? languages.join(", ") : "all languages"}]: it has regular document children`, 400); } await cn.transaction(async (tx) => { await deleteClusterRecursive(tx, siteSchema, languages, { nodeId: rootNodeId, documentType: rootType, }); }); if (rootType.typeName === "home") { await reloadHomeRoutingCluster(siteContext.fqdn); } } async function deleteClusterRecursive(tx, siteSchema, languages, { nodeId, documentType }) { if (documentType.routingChildren) { const routingChildren = await tx("PaNode") .select("id") .select("typeName") .where("parentId", nodeId) .whereIn("typeName", documentType.routingChildren); for (const row of routingChildren) { const child = IdWithTypeNameRowAT.assert(row); await deleteClusterRecursive(tx, siteSchema, languages, { nodeId: child.id, documentType: getDocumentTypeByName(siteSchema, child.typeName), }); } } if (languages) { await tx("PaDocument").where("nodeId", nodeId).whereIn("language", languages).del(); await tx("PaLNode").where("nodeId", nodeId).whereIn("language", languages).del(); } else { await tx("PaDocument").where("nodeId", nodeId).del(); await tx("PaLNode").where("nodeId", nodeId).del(); await tx("PaNode").where("id", nodeId).del(); } } export async function getAllNodeIdsOfRoutingCluster(cn, siteSchema, rootNodeId, rootType) { const nodeIds = []; const queue = [ { id: rootNodeId, typeName: rootType.typeName }, ]; while (queue.length > 0) { const current = queue.shift(); if (!current) break; nodeIds.push(current.id); const currentType = getDocumentTypeByName(siteSchema, current.typeName); if (!currentType.routingChildren || currentType.routingChildren.length === 0) continue; const childRows = await cn("PaNode as n") .select("n.id", "n.typeName") .where("n.parentId", current.id) .whereIn("n.typeName", currentType.routingChildren); for (const row of childRows) { const child = IdWithTypeNameRowAT.assert(row); queue.push({ id: child.id, typeName: child.typeName }); } } return nodeIds; } export async function createRoutingDocumentsInLanguageForCluster(siteContext, tx, cluster, language) { const { siteSchema, logger } = siteContext; const schemaI18n = createSimpleTranslator({ labels: siteSchema.l10n, logger }); if (!cluster.children || cluster.lNodeIds[language]) return; for (const childNode of Object.values(cluster.children)) { await createDocumentsForClusterNode(siteContext, tx, schemaI18n, childNode, language); } } async function createDocumentsForClusterNode(siteContext, tx, schemaI18n, clusterNode, language) { if (!clusterNode.lNodeIds[language]) { const makeTitle = (lang) => schemaI18n.translate({ key: `nodeTypes.${clusterNode.typeName}.label`, language: lang, defaultValue: clusterNode.typeName, }); await tx("PaLNode").insert({ language, nodeId: clusterNode.nodeId, ready: true, updatedAt: tx.raw("current_timestamp"), }); await tx("PaDocument").insert({ language, nodeId: clusterNode.nodeId, title: makeTitle(language), }); } if (clusterNode.children) { for (const childNode of Object.values(clusterNode.children)) { await createDocumentsForClusterNode(siteContext, tx, schemaI18n, childNode, language); } } } //# sourceMappingURL=cluster-management.js.map