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