UNPKG

@paroicms/server

Version:
360 lines 14.8 kB
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