UNPKG

@paroicms/server

Version:
222 lines 8.28 kB
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