@paroicms/server
Version:
The ParoiCMS server
228 lines • 9.89 kB
JavaScript
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