@paroicms/server
Version:
The ParoiCMS server
360 lines • 14.8 kB
JavaScript
import { getDocumentTypeByName, getNodeTypeByName, getRegularDocumentTypeByName, } from "@paroicms/internal-anywhere-lib";
import { generateSlug, isObj, } from "@paroicms/public-anywhere-lib";
import { type } from "arktype";
import mime from "mime";
import { readFile, rm } from "node:fs/promises";
import { basename } from "node:path";
import { createDocumentOnNode, createNodeWithDocument, updateDocument, } from "../admin-backend/document/save-documents.queries.js";
import { saveFieldValues } from "../admin-backend/fields/save-fields.queries.js";
import { addMediaToNode } from "../admin-backend/media/add-media-to-node.js";
import { getTypeNameOf } from "../admin-backend/node/node.queries.js";
import { createNodeWithPart, createPartOnNode } from "../admin-backend/part/part.queries.js";
import { getHandleOfFeaturedImage, getHandleOfField } from "../common/media-handles.helpers.js";
import { createNewRegisteredSite } from "../connector/site-conf/site-conf.js";
import { addRegisteredSite, appConf, platformLogger, registeredSites, removeRegisteredSite, } from "../context.js";
import { siteReadyGuard } from "../graphql/graphql.types.js";
import { hashPassword } from "../helpers/passwordEncrypt-helper.js";
import { getActiveSiteContext, getSiteContext, unloadSiteContext, } from "../site-context/site-context.js";
import { migrateSiteSchemas } from "../site-schema-migration/site-schema-migration.js";
import { createBlankSiteFromExisting } from "./single-site-generator.js";
export function createRunningServerConnector() {
return {
getSitePackConf: (packName) => {
if (appConf.kind !== "multisite") {
throw new Error("getSitePackDirectories is only available in multisite mode");
}
const pack = appConf.sitePacks.find((p) => p.packName === packName);
if (!pack)
throw new Error(`pack "${packName}" not found`);
return {
...pack,
trusted: pack.trusted !== false,
};
},
loadSiteSchemaAndIds: async (fqdn) => {
const siteContext = await getSiteContext(fqdn);
siteReadyGuard(siteContext);
const { siteSchema, homeRoutingCluster } = siteContext;
return { siteSchema, homeRoutingCluster };
},
createAccount,
updateSiteFields,
updateNodeContent,
addMultipleDocumentContents,
addMultiplePartContents,
registerNewSite,
removeSite,
migrateSiteSchemas,
createBlankSiteFromExisting,
};
}
async function createAccount(fqdn, account, options) {
const siteContext = await getSiteContext(fqdn);
siteReadyGuard(siteContext);
const siteNodeId = siteContext.homeRoutingCluster.siteNodeId;
const siteType = siteContext.siteSchema.nodeTypes._site;
const { kind, email, name } = account;
const [inserted] = await siteContext
.cn("PaAccount")
.insert({
email,
name,
passwordHash: kind === "local" ? await hashPassword(account.password) : undefined,
})
.returning("id");
const insertedId = String(type({ id: "number", "+": "reject" }).assert(inserted).id);
if (options?.asContactEmail) {
await saveFieldValues(siteContext, {
typeName: siteType.typeName,
nodeId: siteNodeId,
values: {
contactEmail: email,
},
force: true,
});
}
return insertedId;
}
async function updateSiteFields(fqdn, fields) {
const siteContext = await getSiteContext(fqdn);
siteReadyGuard(siteContext);
const siteNodeId = siteContext.homeRoutingCluster.siteNodeId;
const siteType = siteContext.siteSchema.nodeTypes._site;
const { multilingualFieldValues, fieldMedias } = createFieldValuesToAdd(fields, siteType.fields);
for (const [language, values] of Object.entries(multilingualFieldValues)) {
await saveFieldValues(siteContext, {
typeName: siteType.typeName,
nodeId: siteNodeId,
language,
values,
force: true,
});
}
for (const fieldMedia of fieldMedias) {
await addFieldMedia(siteContext, fieldMedia, siteNodeId, siteType);
}
}
async function updateNodeContent(fqdn, options) {
const { nodeId, content } = options;
const siteContext = await getSiteContext(fqdn);
siteReadyGuard(siteContext);
const typeName = await getTypeNameOf(siteContext, nodeId);
const nodeType = getDocumentTypeByName(siteContext.siteSchema, typeName);
const { multilingualFieldValues, fieldMedias } = createFieldValuesToAdd(content.fields, nodeType.fields ?? []);
if (nodeType.kind !== content.kind) {
throw new Error(`Node "${nodeId}" is of type "${nodeType.kind}", but got "${content.kind}"`);
}
if (content.kind === "document" && content.title) {
for (const [language, title] of Object.entries(content.title)) {
const slug = generateSlug(title);
await updateDocument(siteContext, { nodeId, language }, { title, slug });
}
}
for (const [language, values] of Object.entries(multilingualFieldValues)) {
await saveFieldValues(siteContext, {
typeName,
nodeId,
language,
values,
});
}
if (content.kind === "document" && content.featuredImage && nodeType.withFeaturedImage) {
await addMediaFile(siteContext, {
nodeId,
nodeType,
handle: getHandleOfFeaturedImage(nodeId),
path: content.featuredImage.file,
});
}
for (const fieldMedia of fieldMedias) {
await addFieldMedia(siteContext, fieldMedia, nodeId, nodeType);
}
}
async function addMultipleDocumentContents(fqdn, options) {
const { parentNodeId, contents } = options;
const siteContext = await getSiteContext(fqdn);
siteReadyGuard(siteContext);
const parentType = getNodeTypeByName(siteContext.siteSchema, await getTypeNameOf(siteContext, parentNodeId));
if (parentType.kind !== "document") {
throw new Error(`Cannot add documents as children of "${parentType.typeName}"`);
}
const insertedIds = [];
for (const content of contents) {
const typeName = content.typeName;
if (!parentType.regularChildren?.includes(typeName)) {
throw new Error(`Type "${typeName}" is not a regular child of "${parentType.typeName}"`);
}
const childType = getRegularDocumentTypeByName(siteContext.siteSchema, typeName);
const { multilingualFieldValues, fieldMedias } = createFieldValuesToAdd(content.fields, childType.fields ?? []);
let nodeId;
for (const [language, values] of Object.entries(multilingualFieldValues)) {
const title = content.title?.[language];
const slug = title ? generateSlug(title) : undefined;
if (nodeId === undefined) {
const created = await createNodeWithDocument(siteContext, {
parentId: {
nodeId: parentNodeId,
language,
},
values: { typeName, title, slug },
forceReady: true,
});
nodeId = created.nodeId;
}
else {
await createDocumentOnNode(siteContext, {
language,
nodeId,
values: { title, slug },
forceReady: true,
});
}
await saveFieldValues(siteContext, { typeName, nodeId, language, values });
insertedIds.push({ nodeId, language });
}
if (nodeId !== undefined) {
if (content.featuredImage && childType.withFeaturedImage) {
await addMediaFile(siteContext, {
nodeId,
nodeType: childType,
handle: getHandleOfFeaturedImage(nodeId),
path: content.featuredImage.file,
});
}
for (const fieldMedia of fieldMedias) {
await addFieldMedia(siteContext, fieldMedia, nodeId, childType);
}
}
}
return insertedIds;
}
async function addMultiplePartContents(fqdn, options) {
const { parentNodeId, contents } = options;
const siteContext = await getSiteContext(fqdn);
siteReadyGuard(siteContext);
const parentType = getNodeTypeByName(siteContext.siteSchema, await getTypeNameOf(siteContext, parentNodeId));
if (parentType.kind !== "document" && parentType.kind !== "part") {
throw new Error(`Cannot add parts as children of "${parentType.typeName}"`);
}
const insertedIds = [];
for (const content of contents) {
const typeName = content.typeName;
const listType = parentType.kind === "document"
? parentType.lists?.find((l) => l.parts.includes(typeName))
: parentType.kind === "part"
? parentType.list
: undefined;
if (!listType || !listType.parts.includes(typeName)) {
throw new Error(`Type "${typeName}" is not a part of "${parentType.typeName}"`);
}
const nodeType = getNodeTypeByName(siteContext.siteSchema, typeName);
const { multilingualFieldValues, fieldMedias } = createFieldValuesToAdd(content.fields, nodeType.fields ?? []);
let nodeId;
for (const [language, values] of Object.entries(multilingualFieldValues)) {
if (nodeId === undefined) {
const created = await createNodeWithPart(siteContext, {
parentLNodeId: {
nodeId: parentNodeId,
language,
},
listName: listType.listName,
typeName,
});
nodeId = created.nodeId;
}
else {
await createPartOnNode(siteContext, { language, nodeId });
}
await saveFieldValues(siteContext, { typeName, nodeId, language, values });
insertedIds.push({ nodeId, language });
}
if (nodeId !== undefined) {
for (const fieldMedia of fieldMedias) {
await addFieldMedia(siteContext, fieldMedia, nodeId, nodeType);
}
}
}
return insertedIds;
}
async function addFieldMedia(siteContext, fieldMedia, nodeId, nodeType) {
const { fieldType, language, files } = fieldMedia;
const handle = getHandleOfField(siteContext, {
fieldType,
nodeId,
language,
});
for (const path of files) {
await addMediaFile(siteContext, { nodeId, nodeType, path, handle });
}
}
async function addMediaFile(siteContext, options) {
const { nodeId, nodeType, path, handle } = options;
const filename = basename(path);
const binaryFile = await readFile(path);
const mediaType = mime.getType(path);
if (!mediaType) {
throw new Error(`Cannot determine media type of "${path}"`);
}
await addMediaToNode(siteContext, {
binaryFile,
originalName: filename,
weightB: binaryFile.byteLength,
mediaType,
}, {
nodeId,
nodeType,
handle,
});
}
function createFieldValuesToAdd(fields, fieldTypes) {
const multilingualFieldValues = {};
const medias = [];
for (const fieldType of fieldTypes) {
const content = fields[fieldType.name];
if (!content)
continue;
const { dataType, localized, value } = content;
if (dataType !== fieldType.dataType) {
throw new Error(`Field "${fieldType.name}" has a data type of "${fieldType.dataType}" but got "${dataType}"`);
}
if (localized !== fieldType.localized) {
throw new Error(`Field "${fieldType.name}" should be ${fieldType.localized ? "localized" : "not localized"}"`);
}
if (dataType === "media") {
if (!value)
continue;
if (localized) {
for (const [language, v] of Object.entries(value)) {
if (!v)
continue;
medias.push({ fieldType, files: [v.file], language });
}
}
else {
medias.push({ fieldType, files: [value.file] });
}
}
else if (dataType === "gallery") {
if (!value)
continue;
if (localized) {
for (const [language, v] of Object.entries(value)) {
if (!v)
continue;
medias.push({ fieldType, files: v.files, language });
}
}
else {
medias.push({ fieldType, files: value.files });
}
}
else if (localized) {
for (const [language, v] of Object.entries(value)) {
multilingualFieldValues[language] ??= {};
multilingualFieldValues[language][fieldType.name] = v;
}
}
else {
multilingualFieldValues["."] ??= {};
multilingualFieldValues["."][fieldType.name] = value;
}
}
return { multilingualFieldValues, fieldMedias: medias };
}
async function registerNewSite(options) {
if (appConf.kind !== "multisite") {
throw new Error("registerNewSite is only available in multisite mode");
}
const newRegSite = await createNewRegisteredSite(options, appConf);
addRegisteredSite(newRegSite);
return newRegSite;
}
async function removeSite(fqdn) {
if (appConf.kind !== "multisite") {
throw new Error("removeSite is only available in multisite mode");
}
const regSite = registeredSites.get(fqdn);
if (!regSite)
throw new Error(`unknown site "${fqdn}"`);
removeRegisteredSite(fqdn);
const siteContext = getActiveSiteContext(fqdn, { returnsUndef: true });
if (siteContext) {
await unloadSiteContext(siteContext);
}
const directories = Array.from(new Set([regSite.dataDir, regSite.cacheDir, regSite.backupDir, regSite.siteDir])).sort();
platformLogger.info(`Remove site "${fqdn}": ${directories.join(", ")}`);
for (const dir of directories) {
await rmDirRecursive(dir);
}
}
async function rmDirRecursive(dir) {
try {
await rm(dir, { recursive: true, force: true });
}
catch (error) {
if (!isObj(error) || error.code !== "ENOENT") {
throw error;
}
}
}
//# sourceMappingURL=running-server-connector.js.map