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