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