UNPKG

@paroicms/server

Version:
265 lines (263 loc) 10.1 kB
import { getDocumentTypeByName, getRoutingDocumentTypeByName, isRootOfRoutingCluster, isRootOfSubRoutingCluster, } from "@paroicms/internal-anywhere-lib"; import { encodeLNodeId } from "@paroicms/public-anywhere-lib"; import { type } from "arktype"; const LoadSiteNodeIdAT = type({ id: "number", "+": "reject", }).pipe((r) => ({ id: String(r.id), })); export async function loadSiteNodeId(cn) { const rows = await cn("PaNode") .select("id") .where("parentId", null) .andWhere("typeName", "_site"); if (rows.length === 0) throw new Error("Missing site node in the database"); if (rows.length !== 1) throw new Error(`it should not exist '${rows.length}' site nodes`); const [row] = rows; return LoadSiteNodeIdAT.assert(row).id; } const LoadSiteAndHomeNodeIdAT = type({ siteNodeId: "number", homeNodeId: "number", "+": "reject", }).pipe((r) => ({ siteNodeId: String(r.siteNodeId), homeNodeId: String(r.homeNodeId), })); export async function loadSiteAndHomeNodeId(cn) { const rows = await cn("PaNode as p") .innerJoin("PaNode as c", "c.parentId", "p.id") .select("p.id as siteNodeId", "c.id as homeNodeId") .where("p.parentId", null) .andWhere("p.typeName", "_site"); if (rows.length === 0) throw new Error("Missing site node in the database"); if (rows.length !== 1) throw new Error(`it should not exist '${rows.length}' site nodes`); const [row] = rows; return LoadSiteAndHomeNodeIdAT.assert(row); } export async function loadRoutingCluster(siteContext, clusterRoot) { const { siteSchema, cn } = siteContext; const documentType = getDocumentTypeByName(siteSchema, clusterRoot.typeName); if (clusterRoot.kind === "home") { if (documentType.typeName !== "home") { throw new Error(`Expected home type, got ${documentType.typeName}`); } } else if (documentType.documentKind !== "regular") { throw new Error(`Expected regular document kind, got ${documentType.documentKind}`); } const cluster = await loadClusterNodeBase(siteContext, clusterRoot.nodeId, documentType); const expectedLanguages = Object.keys(cluster.lNodeIds); const childTypes = routingChildrenOf(siteSchema, documentType); const children = childTypes.length > 0 ? await loadClusterNodeChildren(siteContext, expectedLanguages, { parentNodeId: clusterRoot.nodeId, childTypes, }) : undefined; if (clusterRoot.kind === "home") { const siteNodeId = clusterRoot.siteNodeId ?? (await loadSiteNodeId(cn)); return { kind: "home", nodeId: clusterRoot.nodeId, typeName: "home", lNodeIds: cluster.lNodeIds, children, siteNodeId, }; } return { kind: "sub", nodeId: clusterRoot.nodeId, typeName: clusterRoot.typeName, lNodeIds: cluster.lNodeIds, children, }; } async function loadClusterNodeBase(siteContext, nodeId, documentType) { const rows = await createRoutingClusterNodeQuery(siteContext) .where("n.id", nodeId) .where("n.typeName", documentType.typeName); const formattedRows = rows.map((row) => RoutingClusterNodeRowAT.assert(row)); return buildRoutingClusterNode(formattedRows, documentType.typeName); } async function loadClusterNodeChildren(siteContext, expectedLanguages, { parentNodeId, childTypes, }) { const { siteSchema } = siteContext; const rows = await createRoutingClusterNodeQuery(siteContext) .where("n.parentId", parentNodeId) .whereIn("n.typeName", childTypes.map((t) => t.typeName)); const formattedRows = rows.map((row) => RoutingClusterNodeRowAT.assert(row)); const map = new Map(); for (const row of formattedRows) { const typeName = row.typeName; if (!map.has(typeName)) { const routingClusterNode = buildRoutingClusterNode(formattedRows.filter((r) => r.typeName === typeName), typeName); ensureConsistentNodeLanguages(routingClusterNode, expectedLanguages); map.set(typeName, routingClusterNode); } } for (const childType of childTypes) { const ids = map.get(childType.typeName); if (!ids) continue; const subTypes = routingChildrenOf(siteSchema, childType); if (subTypes.length > 0) { ids.children = await loadClusterNodeChildren(siteContext, expectedLanguages, { parentNodeId: ids.nodeId, childTypes: subTypes, }); } } return Object.fromEntries(map); } function ensureConsistentNodeLanguages(clusterNode, expectedLanguages) { const throwError = () => { throw new Error(`Language set inconsistency in cluster: expected languages [${expectedLanguages.join(",")}] but '${clusterNode.nodeId}' has languages [${nodeLanguages.join(",")}]`); }; const nodeLanguages = Object.keys(clusterNode.lNodeIds); if (expectedLanguages.length !== nodeLanguages.length) throwError(); for (const lang of expectedLanguages) { if (!nodeLanguages.includes(lang)) throwError(); } } function routingChildrenOf(siteSchema, documentType) { const typeNames = documentType.routingChildren ?? []; return typeNames.map((typeName) => getRoutingDocumentTypeByName(siteSchema, typeName)); } function createRoutingClusterNodeQuery(siteContext) { const { cn, siteSchema } = siteContext; return cn("PaLNode as l") .select("l.nodeId", "l.language", "n.typeName") .innerJoin("PaNode as n", "n.id", "l.nodeId") .whereIn("l.language", siteSchema.languages); } const RoutingClusterNodeRowAT = type({ nodeId: "number", language: "string", typeName: "string", "+": "reject", }).pipe((r) => ({ nodeId: String(r.nodeId), language: r.language, typeName: r.typeName, })); function buildRoutingClusterNode(rows, typeName) { if (rows.length === 0) { throw new Error(`no rows found for routing cluster node of type '${typeName}'`); } const lNodeIds = {}; let nodeId; for (const row of rows) { nodeId = row.nodeId; lNodeIds[row.language] = encodeLNodeId(row); } if (nodeId === undefined) throw new Error(`missing nodeId for routing cluster '${typeName}'`); return { nodeId, typeName, lNodeIds, }; } export async function loadRoutingClusterFromNode(siteContext, fromNode) { const { siteSchema } = siteContext; const nodeType = siteSchema.nodeTypes[fromNode.typeName]; if (!nodeType) throw new Error(`Unknown node type '${fromNode.typeName}'`); if (nodeType.kind === "site") throw new Error("Cannot load routing cluster from site node"); if (isRootOfSubRoutingCluster(nodeType)) { return await loadRoutingCluster(siteContext, { kind: nodeType.typeName === "home" ? "home" : "sub", nodeId: fromNode.nodeId, typeName: fromNode.typeName, }); } const hierarchy = await loadClusterHierarchy(siteContext, fromNode.nodeId); const clusterRoot = hierarchy[0]; if (!clusterRoot) { throw new Error(`No routing cluster hierarchy for node '${fromNode.nodeId}' of type '${fromNode.typeName}'`); } return await loadRoutingCluster(siteContext, { kind: clusterRoot.typeName === "home" ? "home" : "sub", nodeId: clusterRoot.nodeId, typeName: clusterRoot.typeName, }); } export async function loadParentRoutingClusterFromNode(siteContext, fromNode) { const { siteSchema } = siteContext; const nodeType = siteSchema.nodeTypes[fromNode.typeName]; if (!nodeType) throw new Error(`Unknown node type '${fromNode.typeName}'`); if (nodeType.kind === "site") return; const hierarchy = await loadClusterHierarchy(siteContext, fromNode.nodeId); if (hierarchy.length <= 1) return; const clusterRoot = hierarchy[1]; return await loadRoutingCluster(siteContext, { kind: clusterRoot.typeName === "home" ? "home" : "sub", nodeId: clusterRoot.nodeId, typeName: clusterRoot.typeName, }); } export async function loadRoutingClusterAndParents(siteContext, fromNode) { const hierarchy = await loadClusterHierarchy(siteContext, fromNode.nodeId); if (hierarchy.length === 0) { throw new Error(`No routing cluster hierarchy for node '${fromNode.nodeId}' of type '${fromNode.typeName}'`); } hierarchy.reverse(); const clusters = []; for (const clusterRoot of hierarchy) { const cluster = await loadRoutingCluster(siteContext, { kind: clusterRoot.typeName === "home" ? "home" : "sub", nodeId: clusterRoot.nodeId, typeName: clusterRoot.typeName, }); clusters.push(cluster); } return clusters; } const ClusterNodeHierarchyRowAT = type({ id: "number", typeName: "string", "+": "reject", }).pipe((r) => ({ id: String(r.id), typeName: r.typeName, })); async function loadClusterHierarchy(siteContext, startNodeId) { const { cn, siteSchema } = siteContext; const rows = await cn.raw(` WITH RECURSIVE hierarchy AS ( SELECT id, parentId, typeName FROM PaNode WHERE id = ? UNION ALL -- Recursive case: get parent nodes SELECT p.id, p.parentId, p.typeName FROM PaNode p INNER JOIN hierarchy h ON p.id = h.parentId WHERE p.parentId IS NOT NULL ) SELECT id, typeName FROM hierarchy `, [startNodeId]); const candidates = rows.map((row) => ClusterNodeHierarchyRowAT.assert(row)); const result = []; for (const { id: nodeId, typeName } of candidates) { const documentType = siteSchema.nodeTypes[typeName]; if (documentType && isRootOfRoutingCluster(documentType)) { result.push({ nodeId, typeName }); } } return result; } //# sourceMappingURL=load-routing-cluster.queries.js.map