UNPKG

@paroicms/server

Version:
361 lines 14.8 kB
import { getDocumentTypeByName, getNodeTypeByName, isRootOfRoutingCluster, isRootOfSubRoutingCluster, } from "@paroicms/internal-anywhere-lib"; import { generateSlug, isDef, isValidLanguage, } from "@paroicms/public-anywhere-lib"; import { ApiError, getHandleOfFeaturedImage } from "@paroicms/public-server-lib"; import { type } from "arktype"; import { invalidateDocumentInCache, invalidateDocumentRelationCache, invalidateMultipleDocumentsInCache, } from "../../common/text-cache.js"; import { createNode } from "../../connector/db-init/init-node-queries.js"; import { loadRoutingCluster } from "../../connector/db-init/load-routing-cluster.queries.js"; import { autoCreateCluster } from "../../connector/db-init/populate-routing-documents.js"; import { reloadHomeRoutingCluster } from "../../site-context/refresh-site-context.js"; import { saveHistoryBeforeUpdate } from "../history/history.service.js"; import { updateLNodeUpdatedAt } from "../lnode/lnode.queries.js"; import { countNodeChildrenOf, getLanguagesOfNode, getTypeNameOf } from "../node/node.queries.js"; import { autoCreatePartFieldParts } from "../part/auto-create-part-field-parts.js"; import { createRoutingDocumentsInLanguageForCluster } from "../routing-cluster/cluster-management.js"; import { getParentDocumentIdsOf, getParentLNodeIdOf, getParentNodeIdOf, } from "./load-documents.queries.js"; const UpdateDocumentMetadataValuesAT = type({ "title?": "string|null", "slug?": "string|null", "relativeId?": "string|null", "metaDescription?": "string|null", "metaKeywords?": "string|null", "overrideLanguage?": "string|null", "+": "reject", }); export function parseDocumentMetadataValues(jsonVal) { let parsed; try { parsed = JSON.parse(jsonVal); } catch { throw new ApiError("invalid document metadata values: malformed JSON", 400); } const values = UpdateDocumentMetadataValuesAT.assert(parsed); if (values.overrideLanguage !== undefined && values.overrideLanguage !== null) { if (!isValidLanguage(values.overrideLanguage)) { throw new ApiError("invalid overrideLanguage value", 400); } } return values; } export class CreateDocumentValues { slug; title; } export async function updateDocument(siteContext, documentId, values, options) { const { cn } = siteContext; const { nodeId, language } = documentId; if (!options?.skipHistory) { const typeName = await getTypeNameOf(siteContext, nodeId); await saveHistoryBeforeUpdate(siteContext, { nodeId, language, typeName, }); } await cn("PaDocument") .where({ nodeId: documentId.nodeId, language: documentId.language, }) .update(values); await updateLNodeUpdatedAt(cn, { nodeId, language }); await invalidateDocumentInCache(siteContext, { documentId, perimeter: isDef(values.slug) ? "slug" : undefined, }); } const UpdateNodeParentIdRowAT = type({ parentId: "string|number", "+": "reject", }); export async function updateNodeRelativeId(siteContext, documentId, relativeId) { const { cn } = siteContext; const { nodeId } = documentId; if (generateSlug(relativeId) !== relativeId) { throw new ApiError("invalid relativeId format", 400); } const parentRow = await cn("PaNode").select("parentId").where("id", nodeId).first(); if (!parentRow) throw new ApiError("node not found", 404); const { parentId } = UpdateNodeParentIdRowAT.assert(parentRow); const existing = await cn("PaNode") .select("id") .where({ parentId, relativeId }) .whereNot("id", nodeId) .first(); if (existing) throw new ApiError("relativeId already in use", 409); await cn("PaNode").where("id", nodeId).update({ relativeId }); await invalidateDocumentInCache(siteContext, { documentId, perimeter: "slug", }); } export async function createDocumentOnNode(siteContext, { language, nodeId, values, forceReady, }) { const { cn, siteSchema } = siteContext; const typeName = await getTypeNameOf({ cn: siteContext.cn }, nodeId); const documentType = getDocumentTypeByName(siteSchema, typeName); const autoPublish = documentType.documentKind === "regular" && documentType.autoPublish; const ready = forceReady ?? autoPublish; if (typeName !== "home") { const parentNodeId = await getParentNodeIdOf(siteContext, nodeId); if (parentNodeId === undefined) throw new Error(`missing parent for "${nodeId}"`); const acceptedLanguages = await getLanguagesOfNode(siteContext, parentNodeId); if (!acceptedLanguages.includes(language)) { throw new ApiError(`language "${language}" is not available on parent`, 400); } } const rootOfCluster = isRootOfRoutingCluster(documentType) ? await loadRoutingCluster(siteContext, { kind: typeName === "home" ? "home" : "sub", nodeId, typeName, }) : undefined; const documentId = await cn.transaction(async (tx) => { const result = await createDocumentWithManager({ language, nodeId, cn: tx, values, ready, }); await autoCreatePartFieldParts(siteSchema, { parentNodeId: nodeId, typeName, language, documentNodeId: nodeId, tx, }); if (rootOfCluster) { await createRoutingDocumentsInLanguageForCluster(siteContext, tx, rootOfCluster, language); } return result; }); if (ready) { const parentId = await getParentLNodeIdOf(siteContext, documentId); if (parentId) { await invalidateDocumentRelationCache(siteContext, { documentId: parentId, relation: "children", }); } } if (rootOfCluster && documentType.typeName === "home") { await reloadHomeRoutingCluster(siteContext.fqdn); } return documentId; } export async function createNodeWithDocument(siteContext, { parentId, values, forceReady, }) { const { siteSchema } = siteContext; const { language, nodeId: parentNodeId } = parentId; const documentType = getDocumentTypeByName(siteSchema, values.typeName); const autoPublish = documentType.documentKind === "regular" && documentType.autoPublish; const ready = forceReady ?? autoPublish; const parentTypeName = await getTypeNameOf(siteContext, parentNodeId); const parentType = getNodeTypeByName(siteContext.siteSchema, parentTypeName); if (parentType.kind === "document" && parentType.childLimit !== undefined) { const count = await countNodeChildrenOf(siteContext, parentNodeId); if (count >= parentType.childLimit) throw new ApiError("maximum children reached", 400); } if (parentType.kind !== "site") { const acceptedLanguages = await getLanguagesOfNode(siteContext, parentNodeId); if (!acceptedLanguages.includes(language)) { throw new ApiError(`language "${language}" is not available on parent`, 400); } } const documentId = await siteContext.cn.transaction(async (tx) => { let providedRelativeId; if (documentType.documentKind === "regular" && documentType.route === ":relativeId") { if (!values.relativeId) throw new ApiError("relativeId is required", 400); if (generateSlug(values.relativeId) !== values.relativeId) { throw new ApiError("invalid relativeId format", 400); } const existing = await tx("PaNode") .select("id") .where({ parentId: parentNodeId, relativeId: values.relativeId }) .first(); if (existing) throw new ApiError("relativeId already in use", 409); providedRelativeId = values.relativeId; } const { id: nodeId } = await createNode(siteContext.siteSchema, tx, { parentId: parentNodeId, typeName: values.typeName, relativeId: providedRelativeId, }); const newId = await createDocumentWithManager({ language, nodeId, values, cn: tx, ready, }); await autoCreatePartFieldParts(siteContext.siteSchema, { parentNodeId: nodeId, typeName: values.typeName, language, documentNodeId: nodeId, tx, }); return newId; }); if (ready) { await invalidateDocumentRelationCache(siteContext, { documentId: parentId, relation: "children", }); } if (isRootOfSubRoutingCluster(documentType)) { await autoCreateCluster(siteContext, { nodeId: documentId.nodeId, documentType, }); } return documentId; } const DeleteDocumentNodeRowAT = type({ id: "number", "+": "reject", }).pipe((r) => ({ id: String(r.id), })); export async function deleteNodeAndDocuments(siteContext, nodeId) { const { cn, mediaStorage } = siteContext; const parentDocumentIds = await getParentDocumentIdsOf(siteContext, { nodeId }); const { partNodeIds } = await cn.transaction(async (tx) => { const nodeRows = await tx("PaNode as n") .select("n.id as id") .whereRaw("n.parentId = ?", [nodeId]) .whereNotExists(function () { this.select(1).from("PaDocument as d").where("d.nodeId", "n.id"); }); const partNodeIds = nodeRows.map((r) => DeleteDocumentNodeRowAT.assert(r).id); await fullDeleteParts(partNodeIds, tx); await tx("PaFieldVarchar").where("nodeId", nodeId).delete(); await tx("PaFieldText").where("nodeId", nodeId).delete(); await tx("PaDocument").where("nodeId", nodeId).delete(); await tx("PaLNode").where("nodeId", nodeId).delete(); await tx("PaNode").where("id", nodeId).delete(); return { partNodeIds }; }); await mediaStorage.deleteMedias({ handles: [...partNodeIds.map(getHandleOfFeaturedImage), getHandleOfFeaturedImage(nodeId)], }); await invalidateMultipleDocumentsInCache(siteContext, { nodeId, parentIds: parentDocumentIds, fully: true, }); } const DeleteDocumentCountRowAT = type({ cnt: "number", "+": "reject", }); export async function deleteDocument(siteContext, documentId) { const { cn, mediaStorage } = siteContext; const { nodeId, language } = documentId; const parentId = await getParentLNodeIdOf(siteContext, documentId); const countRow = await cn("PaLNode as l") .count("l.nodeId as cnt") .where("l.nodeId", nodeId) .andWhereNot("language", documentId.language) .first(); const count = countRow ? DeleteDocumentCountRowAT.assert(countRow).cnt : 0; const shouldDeleteNode = count === 0; const { partNodeIds } = await cn.transaction(async (tx) => { const nodeRows = await tx("PaNode as n") .select("n.id as id") .whereRaw("n.parentId = ?", [nodeId]) .whereNotExists(function () { this.select(1).from("PaDocument as d").where("d.nodeId", "n.id"); }); const partNodeIds = nodeRows.map((r) => DeleteDocumentNodeRowAT.assert(r).id); await deleteParts({ language, nodeIdList: partNodeIds, cn: tx, }); await tx("PaFieldVarchar") .where("nodeId", documentId.nodeId) .andWhere("language", documentId.language) .delete(); await tx("PaFieldText") .where("nodeId", documentId.nodeId) .andWhere("language", documentId.language) .delete(); await tx("PaDocument") .where("nodeId", documentId.nodeId) .andWhere("language", documentId.language) .delete(); await tx("PaLNode") .where("nodeId", documentId.nodeId) .andWhere("language", documentId.language) .delete(); if (shouldDeleteNode) { await tx("PaFieldVarchar").where("nodeId", nodeId).delete(); await tx("PaFieldText").where("nodeId", nodeId).delete(); await tx("PaFieldLabeling").where("nodeId", nodeId).orWhere("termId", nodeId).delete(); await tx("PaPartNode").where("documentNodeId", nodeId).delete(); await tx("PaNode").where("id", nodeId).delete(); } return { partNodeIds }; }); await invalidateDocumentInCache(siteContext, { documentId, fully: true, parentId }); if (shouldDeleteNode) { const handles = partNodeIds.map(getHandleOfFeaturedImage); handles.push(getHandleOfFeaturedImage(nodeId)); const deletedIds = await mediaStorage.deleteMedias({ handles }); await siteContext.imageCache.invalidate({ mediaIds: deletedIds }); } } const DeletePartCountRowAT = type({ cnt: "number", "+": "reject", }); async function deleteParts({ nodeIdList, cn, language, }) { if (nodeIdList.length === 0) return; await cn("PaLNode").whereIn("nodeId", nodeIdList).andWhere("language", language).delete(); for (const nodeId of nodeIdList) { const countRow = await cn("PaLNode as nl") .count("nl.nodeId as cnt") .where("nl.nodeId", nodeId) .first(); if (!countRow) continue; if (DeletePartCountRowAT.assert(countRow).cnt === 0) { await cn("PaFieldVarchar").where("nodeId", nodeId).delete(); await cn("PaFieldText").where("nodeId", nodeId).delete(); await cn("PaNode").where("id", nodeId).delete(); } } } async function fullDeleteParts(nodeIdList, cn) { if (nodeIdList.length === 0) return; await cn("PaFieldText").whereIn("nodeId", nodeIdList).delete(); await cn("PaFieldVarchar").whereIn("nodeId", nodeIdList).delete(); await cn("PaLNode").whereIn("nodeId", nodeIdList).delete(); await cn("PaNode").whereIn("id", nodeIdList).delete(); } async function createDocumentWithManager({ language, nodeId, values, ready, cn, }) { await cn("PaLNode").insert({ language, nodeId, ready: !!ready, updatedAt: cn.raw("current_timestamp"), }); await cn("PaDocument").insert({ language, nodeId, slug: values?.slug, title: values?.title, }); return { language, nodeId }; } //# sourceMappingURL=save-documents.queries.js.map