UNPKG

@paroicms/server

Version:
234 lines 9.38 kB
import { getNodeTypeByName } from "@paroicms/internal-anywhere-lib"; import { isDef, isJsonFieldValue, isObj, isUpdateLabelingValue, } from "@paroicms/public-anywhere-lib"; import { ApiError, stripHtmlTags } from "@paroicms/public-server-lib"; import { type } from "arktype"; import { invalidateDocumentRelationCache, invalidateMultipleDocumentsInCache, } from "../../common/text-cache.js"; import { dbAnyLanguage } from "../../context.js"; import { truncExcerptToWord } from "../../helpers/excerpt.helpers.js"; import { createNoRenderingContext } from "../../liquidjs-tools/liquidjs-rendering/rendering-context.js"; import { executeBeforeSaveValues } from "../../plugin-services/before-save-values-hook.js"; import { executeRenderingHookForExcerpt } from "../../plugin-services/rendering-hook.js"; import { loadSiteAccess } from "../../site-context/site-values-for-site-context.js"; import { saveHistoryBeforeUpdate } from "../history/history.service.js"; import { updateLNodeUpdatedAt } from "../lnode/lnode.queries.js"; import { languageIfLNode } from "./_fields.helpers.js"; import { saveFieldLabeling } from "./labeling.queries.js"; const StringAT = type("string"); const NumberAT = type("number"); const BooleanAT = type("boolean"); export function parseFieldValues(jsonVal) { let obj; try { obj = JSON.parse(jsonVal); } catch { } if (!obj || !isObj(obj)) throw new ApiError("invalid fieldValues", 400); return obj; } export async function saveFieldValues(siteContext, options) { const { typeName, nodeId, skipHistory } = options; if (!skipHistory && typeName !== "_site" && options.language) { await saveHistoryBeforeUpdate(siteContext, { nodeId, language: options.language, typeName, }); } const values = await executeBeforeSaveValues(siteContext, { typeName, language: options.language, values: options.values, }); const nodeType = getNodeTypeByName(siteContext.siteSchema, typeName); await saveFieldValuesInMainDb(siteContext, { ...options, values, nodeType, }); if (options.language) { const hasNonLocalizedField = nodeType.fields?.some((f) => !f.localized && Object.keys(values).includes(f.name)); if (hasNonLocalizedField) { await updateLNodeUpdatedAt(siteContext.cn, { nodeId }); } else { await updateLNodeUpdatedAt(siteContext.cn, { nodeId, language: options.language }); } } if (nodeType.kind === "part") { await invalidateMultipleDocumentsInCache(siteContext, { parentOfNodeId: nodeId }); } else if (nodeType.kind === "document") { await invalidateMultipleDocumentsInCache(siteContext, { nodeId }); } else if (nodeType.kind === "site") { await siteContext.textCache.invalidateAll(); } if (nodeType.kind === "site" && "access" in values) { siteContext.access = await loadSiteAccess(siteContext); siteContext.logger.info(`Site access updated: ${siteContext.access.access}`); } } async function saveFieldValuesInMainDb(siteContext, options) { const { nodeId, language, values, nodeType, force } = options; const fieldTypes = nodeType.fields ?? []; const typeMap = new Map(fieldTypes.map((item) => [item.name, item])); const excerpts = await generateAllExcerpts(siteContext, { values, typeMap }); const labelingFields = []; await siteContext.cn.transaction(async (tx) => { for (const [fieldName, value] of Object.entries(values)) { if (value === undefined) continue; const fieldType = typeMap.get(fieldName); if (!fieldType) throw new Error(`should have the field type of '${fieldName}'`); if (!force && fieldType.readOnly) { throw new Error(`Attempt to update read-only ${fieldType.name}`); } switch (fieldType.storedAs) { case "varchar": await saveVarcharFieldValue(tx, { nodeId, language, fieldType, value }); break; case "text": await saveTextFieldValue(tx, { nodeId, language, fieldType, value, excerpts, }); break; case "labeling": { const termNodeIds = isUpdateLabelingValue(value) ? value.t : null; await saveFieldLabeling(tx, { fieldType, nodeId, termNodeIds, }); if (termNodeIds) { labelingFields.push({ fieldType, termNodeIds }); } break; } case "partField": case "mediaHandle": throw new Error(`shouldn't include '${fieldType.storedAs}' field '${fieldName}' for saving`); default: throw new Error(`invalid field type '${fieldType.storedAs}'`); } } }); for (const { fieldType, termNodeIds } of labelingFields) { for (const termNodeId of termNodeIds) { await invalidateDocumentRelationCache(siteContext, { relation: "labeled", termNodeId, fieldName: fieldType.name, }); } } } async function generateAllExcerpts(siteContext, options) { const { values, typeMap } = options; const renderingContext = createNoRenderingContext(siteContext); const excerpts = {}; for (const [fieldName, value] of Object.entries(values)) { if (value === undefined || value === null) continue; const fieldType = typeMap.get(fieldName); if (!fieldType) throw new Error(`should have the field type of '${fieldName}'`); if (fieldType.renderAs !== "html" || !fieldType.useAsExcerpt) continue; if (!isJsonFieldValue(value) && typeof value !== "string") { throw new Error(`should be a json or string value '${fieldName}'`); } let plainText = await executeRenderingHookForExcerpt(renderingContext, { value, fieldType, }); if (fieldType.renderAs === "html" && fieldType.dataType === "string" && plainText === value) { plainText = stripHtmlTags(StringAT.assert(value), { blockSeparator: " – " }); } const excerpt = plainText ? truncExcerptToWord(plainText, 299) : undefined; excerpts[fieldName] = excerpt; } await renderingContext.close(); return excerpts; } async function saveVarcharFieldValue(cn, options) { const { nodeId, fieldType, value } = options; const dbLanguage = languageIfLNode(options.language, fieldType) ?? dbAnyLanguage; const val = fieldValueToDbString(value, fieldType.dataType); if (val === null) { await cn("PaFieldVarchar") .where({ field: fieldType.name, nodeId: nodeId, language: dbLanguage, }) .delete(); } else { await cn("PaFieldVarchar") .insert({ field: fieldType.name, nodeId, language: dbLanguage, dataType: fieldType.dataType, val, plugin: fieldType.pluginName ?? null, }) .onConflict(["field", "nodeId", "language"]) .merge(["dataType", "val", "plugin"]); } } async function saveTextFieldValue(cn, options) { const { nodeId, fieldType, value, excerpts } = options; const dbLanguage = languageIfLNode(options.language, fieldType) ?? dbAnyLanguage; const val = fieldValueToDbString(value, fieldType.dataType); if (val === null) { await cn("PaFieldText") .where({ field: fieldType.name, nodeId: nodeId, language: dbLanguage, }) .delete(); } else { await cn("PaFieldText") .insert({ field: fieldType.name, nodeId, language: dbLanguage, dataType: fieldType.dataType, val, excerpt: excerpts[fieldType.name], plugin: fieldType.pluginName ?? null, }) .onConflict(["field", "nodeId", "language"]) .merge(["dataType", "val", "excerpt", "plugin"]); } } function fieldValueToDbString(value, dataType) { if (value === null) return value; if (dataType === "number") return String(NumberAT.assert(value)); if (dataType === "boolean") return BooleanAT.assert(value) ? "1" : "0"; if (dataType === "time") return StringAT.assert(value); if (dataType === "date") return StringAT.assert(value); if (dataType === "dateTime") return StringAT.assert(value); if (dataType === "string") return StringAT.assert(value); if (dataType === "json") return isJsonFieldValue(value) && isDef(value.j) ? JSON.stringify(value.j) : null; throw new Error(`invalid data type '${dataType}'`); } //# sourceMappingURL=save-fields.queries.js.map