@paroicms/server
Version:
The ParoiCMS server
265 lines (263 loc) • 10.1 kB
JavaScript
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