@paroicms/server
Version:
The ParoiCMS server
222 lines • 8.28 kB
JavaScript
import { getNodeTypeByName } from "@paroicms/internal-anywhere-lib";
import { isJsonFieldValue, isMediaHandleValue, isReadLabelingValue, unwrapReadFieldValues, } from "@paroicms/public-anywhere-lib";
import { ApiError } from "@paroicms/public-server-lib";
import { document } from "../document/load-documents.queries.js";
import { updateDocument } from "../document/save-documents.queries.js";
import { getFieldTypesByNames, loadFieldValues } from "../fields/load-fields.queries.js";
import { saveFieldValues } from "../fields/save-fields.queries.js";
import { getTypeNameOf } from "../node/node.queries.js";
import { computeChangeSummary } from "./change-summary.js";
import { createHistoryEntry, enforceEntryLimit, getHistoryEntry, getLatestHistoryEntryInTiers, } from "./history.queries.js";
import { ORDERED_TIERS } from "./history.types.js";
const TIER_THRESHOLDS = {
short: 5,
medium: 120,
long: 30 * 24 * 60,
};
function getExpireAtModifier(tier) {
switch (tier) {
case "short":
return "+24 hours";
case "medium":
return "+60 days";
case "long":
return null;
}
}
const MAX_ENTRIES_PER_LNODE = 100;
export async function saveHistoryBeforeUpdate(siteContext, options) {
const { nodeId, language, typeName, force } = options;
if (typeName === "_site")
return;
const nodeType = getNodeTypeByName(siteContext.siteSchema, typeName);
const isDocumentType = nodeType.kind === "document";
const { currentFieldValues, currentDocument } = await loadCurrentState(siteContext, {
nodeId,
language,
typeName,
isDocumentType,
});
if (force) {
await saveForceHistoryEntry(siteContext, {
nodeId,
language,
isDocumentType,
currentFieldValues,
currentDocument,
});
return;
}
await saveTieredHistoryEntry(siteContext, {
nodeId,
language,
isDocumentType,
currentFieldValues,
currentDocument,
});
}
async function saveForceHistoryEntry(siteContext, options) {
const { nodeId, language, isDocumentType, currentFieldValues, currentDocument } = options;
const currentDocumentValues = documentRowToHistoryValues(currentDocument, isDocumentType);
const { fieldValues, documentValues } = serializeCurrentState(currentFieldValues, currentDocumentValues);
const latestEntry = await getLatestHistoryEntryInTiers(siteContext, {
nodeId,
language,
minTier: "short",
});
const changeSummary = computeChangeSummary({
currentFieldValues,
previousFieldValues: latestEntry?.fieldValues,
currentDocumentValues,
previousDocumentValues: latestEntry?.documentValues
? JSON.parse(latestEntry.documentValues)
: undefined,
});
if (!changeSummary)
return;
await createHistoryEntry(siteContext, {
nodeId,
language,
fieldValues,
documentValues,
changeSummary: JSON.stringify({ ...changeSummary, beforeRestore: true }),
expireAt: getExpireAtModifier("short"),
timeTier: "short",
});
await enforceEntryLimit(siteContext, { nodeId, language, maxEntries: MAX_ENTRIES_PER_LNODE });
}
async function saveTieredHistoryEntry(siteContext, options) {
const { nodeId, language, isDocumentType, currentFieldValues, currentDocument } = options;
const tierResult = await selectTierForEntry(siteContext, { nodeId, language });
if (!tierResult)
return;
const previousEntry = await getLatestHistoryEntryInTiers(siteContext, {
nodeId,
language,
minTier: tierResult.tier,
});
const currentDocumentValues = documentRowToHistoryValues(currentDocument, isDocumentType);
const changeSummary = computeChangeSummary({
currentFieldValues,
previousFieldValues: previousEntry?.fieldValues,
currentDocumentValues,
previousDocumentValues: previousEntry?.documentValues
? JSON.parse(previousEntry.documentValues)
: undefined,
});
if (!changeSummary)
return;
const { fieldValues, documentValues } = serializeCurrentState(currentFieldValues, currentDocumentValues);
await createHistoryEntry(siteContext, {
nodeId,
language,
fieldValues,
documentValues,
changeSummary: JSON.stringify(changeSummary),
expireAt: tierResult.expireAt,
timeTier: tierResult.tier,
});
await enforceEntryLimit(siteContext, { nodeId, language, maxEntries: MAX_ENTRIES_PER_LNODE });
}
async function selectTierForEntry(siteContext, options) {
const { nodeId, language } = options;
for (const tier of [...ORDERED_TIERS].reverse()) {
const thresholdMinutes = TIER_THRESHOLDS[tier];
const recentEntry = await getLatestHistoryEntryInTiers(siteContext, {
nodeId,
language,
minTier: tier,
thresholdMinutes,
});
if (!recentEntry) {
return { tier, expireAt: getExpireAtModifier(tier) };
}
}
}
async function loadCurrentState(siteContext, options) {
const { nodeId, language, typeName, isDocumentType } = options;
const fieldTypes = getFieldTypesByNames(siteContext, { typeName });
const currentFieldValues = await loadFieldValues(siteContext, {
nodeId,
language,
fieldTypes,
publishedOnly: false,
});
let currentDocument;
if (isDocumentType) {
currentDocument = await document(siteContext, { nodeId, language });
}
return { currentFieldValues, currentDocument };
}
function serializeCurrentState(currentFieldValues, currentDocumentValues) {
const fieldValues = JSON.stringify(unwrapReadFieldValues(currentFieldValues));
const documentValues = currentDocumentValues ? JSON.stringify(currentDocumentValues) : null;
return { fieldValues, documentValues };
}
function documentRowToHistoryValues(doc, isDocumentType) {
if (!isDocumentType || !doc)
return;
return {
title: doc.title,
slug: doc.slug,
metaDescription: doc.metaDescription,
metaKeywords: doc.metaKeywords,
ready: doc.ready,
};
}
export async function restoreHistoryEntry(siteContext, options) {
const { entryId } = options;
const entry = await getHistoryEntry(siteContext, entryId);
if (!entry) {
throw new ApiError("history entry not found", 404);
}
const { nodeId, language, fieldValues, documentValues } = entry;
const typeName = await getTypeNameOf(siteContext, nodeId);
await saveHistoryBeforeUpdate(siteContext, {
nodeId,
language,
typeName,
force: true,
});
const updateValues = convertReadFieldValuesToUpdateValues(fieldValues);
await saveFieldValues(siteContext, {
typeName,
nodeId,
language,
values: updateValues,
skipHistory: true,
});
if (documentValues) {
const parsedDocValues = JSON.parse(documentValues);
await updateDocument(siteContext, { nodeId, language }, {
title: parsedDocValues.title,
slug: parsedDocValues.slug,
metaDescription: parsedDocValues.metaDescription,
metaKeywords: parsedDocValues.metaKeywords,
}, { skipHistory: true });
}
}
function convertReadFieldValuesToUpdateValues(readValues) {
const result = {};
for (const [key, value] of Object.entries(readValues)) {
if (value === undefined)
continue;
result[key] = convertSingleValue(value);
}
return result;
}
function convertSingleValue(value) {
if (value === null)
return null;
if (isMediaHandleValue(value))
return undefined;
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return value;
}
if (isReadLabelingValue(value))
return { t: value.t.map((term) => term.nodeId) };
if (isJsonFieldValue(value))
return value;
throw new Error(`unsupported field value type in history restoration: ${JSON.stringify(value)}`);
}
//# sourceMappingURL=history.service.js.map