UNPKG

@paroicms/server

Version:
554 lines 19.7 kB
import { documentTypeHasData, getDocumentTypeByName, getNodeTypeByName, } from "@paroicms/internal-anywhere-lib"; import { parseSqliteDateTime } from "@paroicms/internal-server-lib"; import { ApiError, createSimpleTranslator } from "@paroicms/public-server-lib"; import { type } from "arktype"; import { applyRegularChildrenSortingOnQuery } from "../../common/child-ordering-query.js"; import { toDocumentSeed } from "../../common/data-format.js"; import { invalidateDocumentInCache, invalidateDocumentRelationCache, } from "../../common/text-cache.js"; import { createNoRenderingContext } from "../../liquidjs-tools/liquidjs-rendering/rendering-context.js"; import { listDocValues } from "../../rendering-payload/doc-list.queries.js"; import { getParentLNodeIdOf } from "../document/load-documents.queries.js"; import { getNodeAncestorsForBreadcrumb } from "../node/node-cte.queries.js"; import { getTypeNameOf } from "../node/node.queries.js"; import { getDocumentNodeIdOfPart } from "../part/part.queries.js"; const FindOneDocumentRowAT = type({ nodeId: "number", language: "string", ready: "number", updatedAt: "string|number|Date", title: "string|null", slug: "string|null", metaDescription: "string|null", metaKeywords: "string|null", overrideLanguage: "string|null", "+": "reject", }).pipe((data) => ({ nodeId: String(data.nodeId), language: data.language, ready: Boolean(data.ready), updatedAt: parseSqliteDateTime(data.updatedAt), title: data.title ?? undefined, slug: data.slug ?? undefined, metaDescription: data.metaDescription ?? undefined, metaKeywords: data.metaKeywords ?? undefined, overrideLanguage: data.overrideLanguage ?? undefined, })); export async function findOneDocumentSeed({ cn }, lNodeId) { const found = await cn("PaLNode as l") .leftJoin("PaDocument as d", function () { this.on("d.nodeId", "l.nodeId").andOn("d.language", "l.language"); }) .select([ "l.nodeId", "l.language", "l.ready", "l.updatedAt", "d.title", "d.slug", "d.metaDescription", "d.metaKeywords", "d.overrideLanguage", ]) .where("l.nodeId", lNodeId.nodeId) .andWhere("l.language", lNodeId.language) .first(); if (!found) { throw new ApiError(`can't find document ${lNodeId.language}:${lNodeId.nodeId}`, 404); } const row = FindOneDocumentRowAT.assert(found); return toDocumentSeed(row); } export async function getDocuments(siteContext, { parentId, pagination, sorting, }) { let total; let documentRows; if (pagination) { const childResult = await getChildLNodesForAllLanguages(siteContext, { parentNodeId: parentId.nodeId, pagination, sorting, }); documentRows = childResult.list; total = childResult.total; } else { documentRows = await getChildLNodesForAllLanguages(siteContext, { parentNodeId: parentId.nodeId, pagination, sorting, }); } const byNodeIdMap = new Map(); for (const row of documentRows) { let list = byNodeIdMap.get(row.nodeId); if (!list) { list = []; byNodeIdMap.set(row.nodeId, list); } list.push(row); } const result = []; for (const list of byNodeIdMap.values()) { let found = list.find((i) => i.language === parentId.language); found ??= list[0]; result.push(found); } return { total, documents: result.map(toDocumentSeed), }; } const ChildDocumentRowAT = type({ nodeId: "number", language: "string", ready: "number", updatedAt: "string|number|Date", title: "string|null", slug: "string|null", metaDescription: "string|null", metaKeywords: "string|null", overrideLanguage: "string|null", "+": "reject", }).pipe((data) => ({ nodeId: String(data.nodeId), language: data.language, ready: Boolean(data.ready), updatedAt: parseSqliteDateTime(data.updatedAt), title: data.title ?? undefined, slug: data.slug ?? undefined, metaDescription: data.metaDescription ?? undefined, metaKeywords: data.metaKeywords ?? undefined, overrideLanguage: data.overrideLanguage ?? undefined, })); const ChildLNodesCountRowAT = type({ cnt: "number", "+": "reject", }); async function getChildLNodesForAllLanguages(siteContext, options) { const parentTypeName = await getTypeNameOf(siteContext, options.parentNodeId); const parentDocumentType = getDocumentTypeByName(siteContext.siteSchema, parentTypeName); if (options.pagination) { return await getChildLNodesWithPagination(siteContext, { parentNodeId: options.parentNodeId, parentDocumentType, pagination: options.pagination, sorting: options.sorting, }); } return await getChildLNodesWithoutPagination(siteContext, { parentNodeId: options.parentNodeId, parentDocumentType, sorting: options.sorting, }); } async function getChildLNodesWithPagination(siteContext, { parentNodeId, parentDocumentType, pagination, sorting, }) { const nodeIds = await fetchPaginatedNodeIds(siteContext, { parentNodeId, parentDocumentType, pagination, sorting, }); if (nodeIds.length === 0) { return { list: [], total: pagination.offset, }; } const lNodeRows = await fetchDocumentRowsForNodeIds(siteContext, nodeIds); const total = await countDistinctChildNodes(siteContext, { parentNodeId, parentDocumentType, nodeIdsCount: nodeIds.length, pagination, }); return { list: lNodeRows, total, }; } async function fetchPaginatedNodeIds(siteContext, { parentNodeId, parentDocumentType, pagination, sorting, }) { const nodeIdsSubquery = createBaseChildLNodesForAllLanguagesQueryBuilder(siteContext, { parentNodeId, typeNames: parentDocumentType.regularChildren ?? [], }).select("n.id as nodeId"); applyRegularChildrenSortingOnQuery(siteContext, { query: nodeIdsSubquery, parentDocumentType, leftJoinDocument: true, sorting, }); nodeIdsSubquery.groupBy("n.id").limit(pagination.limit).offset(pagination.offset); const nodeIdRows = await nodeIdsSubquery; return nodeIdRows.map((row) => String(row.nodeId)); } async function fetchDocumentRowsForNodeIds(siteContext, nodeIds) { const lNodesQuery = siteContext .cn("PaLNode as l") .leftJoin("PaDocument as d", function () { this.on("d.nodeId", "l.nodeId").andOn("d.language", "l.language"); }) .select([ "l.nodeId", "l.language", "l.ready", "l.updatedAt", "d.title", "d.slug", "d.metaDescription", "d.metaKeywords", "d.overrideLanguage", ]) .whereIn("l.nodeId", nodeIds); const rows = await lNodesQuery; return rows.map((row) => ChildDocumentRowAT.assert(row)); } async function countDistinctChildNodes(siteContext, { parentNodeId, parentDocumentType, nodeIdsCount, pagination, }) { if (nodeIdsCount < pagination.limit) { return pagination.offset + nodeIdsCount; } const countQuery = createBaseChildLNodesForAllLanguagesQueryBuilder(siteContext, { parentNodeId, typeNames: parentDocumentType.regularChildren ?? [], }) .countDistinct("n.id as cnt") .first(); const countResult = await countQuery; return countResult ? ChildLNodesCountRowAT.assert(countResult).cnt : 0; } async function getChildLNodesWithoutPagination(siteContext, { parentNodeId, parentDocumentType, sorting, }) { const query = createBaseChildLNodesForAllLanguagesQueryBuilder(siteContext, { parentNodeId, typeNames: parentDocumentType.regularChildren ?? [], }) .leftJoin("PaDocument as d", function () { this.on("d.nodeId", "l.nodeId").andOn("d.language", "l.language"); }) .select([ "l.nodeId", "l.language", "l.ready", "l.updatedAt", "d.title", "d.slug", "d.metaDescription", "d.metaKeywords", "d.overrideLanguage", ]); applyRegularChildrenSortingOnQuery(siteContext, { query, parentDocumentType, leftJoinDocument: false, sorting, }); const rows = await query; return rows.map((row) => ChildDocumentRowAT.assert(row)); } function createBaseChildLNodesForAllLanguagesQueryBuilder(siteContext, { parentNodeId, typeNames, }) { return siteContext .cn("PaLNode as l") .innerJoin("PaNode as n", "n.id", "l.nodeId") .where("n.parentId", parentNodeId) .whereIn("n.typeName", typeNames); } export async function setLNodeReady(siteContext, { lNodeId, ready }) { const typeName = await getTypeNameOf(siteContext, lNodeId.nodeId); const nodeType = getNodeTypeByName(siteContext.siteSchema, typeName); if (nodeType.kind !== "document" && nodeType.kind !== "part") { throw new Error(`invalid lNode type '${typeName}'`); } await siteContext.cn("PaLNode").update({ ready }).where({ nodeId: lNodeId.nodeId, language: lNodeId.language, }); if (nodeType.kind === "document") { const parentId = await getParentLNodeIdOf(siteContext, lNodeId); if (ready) { if (parentId) { await invalidateDocumentRelationCache(siteContext, { documentId: parentId, relation: "children", }); } } else { await invalidateDocumentInCache(siteContext, { documentId: lNodeId, fully: true, parentId, }); } } else { const documentId = { nodeId: await getDocumentNodeIdOfPart(siteContext, lNodeId.nodeId), language: lNodeId.language, }; await invalidateDocumentInCache(siteContext, { documentId }); } } export async function getBreadcrumb(siteContext, tracker, documentId, options = {}) { const { siteSchema, logger } = siteContext; const schemaI18n = createSimpleTranslator({ labels: siteSchema.l10n, logger }); const ancestors = await getNodeAncestorsForBreadcrumb(siteContext, tracker, documentId, { ensurePublished: options.ensurePublished, }); ancestors.reverse(); if (options.skipHome) { ancestors.shift(); } if (options.skipCurrent) { ancestors.pop(); } const result = []; for (const ancestor of ancestors) { const documentType = getDocumentTypeByName(siteContext.siteSchema, ancestor.typeName); const hasData = documentTypeHasData(documentType); if (options.ensurePublished && !hasData) continue; result.push({ documentId: { nodeId: ancestor.nodeId, language: documentId.language }, typeName: ancestor.typeName, title: hasData ? ancestor.title : schemaI18n.translate({ key: `nodeTypes.${documentType.typeName}.label`, language: documentId.language, defaultValue: documentType.typeName, }), }); } return result; } const OrphanNodeRowAT = type({ cnt: "number", "+": "reject", }); export async function isOrphanNode(cn, nodeId) { const row = (await cn("PaLNode as l") .count("l.nodeId as cnt") .where("l.nodeId", nodeId) .first()); return row ? OrphanNodeRowAT.assert(row).cnt === 0 : true; } export async function searchDocuments(siteContext, input) { const { queryString, language, offset, limit, sorting } = input; const words = queryString.split(/\s+/).filter((word) => word.length >= 2); if (words.length === 0) return { total: 0, offset, items: [] }; const descriptor = { load: "list", nodeKind: "document", descriptorName: "search", language, words, offset, limit, sorting, }; const renderingContext = createNoRenderingContext(siteContext); const { items: rows, total } = await listDocValues(renderingContext, descriptor, { onlyPublished: false, withTotal: true, }); const documentSeeds = []; for (const row of rows) { documentSeeds.push(await findOneDocumentSeed(siteContext, { language: row.language, nodeId: row.nodeId, })); } return { total, items: documentSeeds, offset, }; } const LNodeLanguageRowAT = type({ language: "string", "+": "reject", }); export async function getParentLanguages(siteContext, nodeId) { const parentLanguages = await siteContext .cn("PaLNode as p") .select("p.language") .innerJoin("PaNode as c", "c.parentId", "p.nodeId") .where("c.id", nodeId); return parentLanguages.map((item) => LNodeLanguageRowAT.assert(item).language); } export async function updateLNodeUpdatedAt(cn, options) { if (options.language) { await cn("PaLNode") .where({ nodeId: options.nodeId, language: options.language }) .update({ updatedAt: cn.raw("current_timestamp") }); } else { await cn("PaLNode") .where({ nodeId: options.nodeId }) .update({ updatedAt: cn.raw("current_timestamp") }); } } export async function getChildDocuments(siteContext, options) { const parentTypeName = await getTypeNameOf(siteContext, options.parentNodeId); const parentDocumentType = getDocumentTypeByName(siteContext.siteSchema, parentTypeName); const typeNames = parentDocumentType.regularChildren ?? []; if (options.missingInLanguage) { return await getChildDocumentsMissingLanguage(siteContext, { parentNodeId: options.parentNodeId, language: options.language, typeNames, parentDocumentType, pagination: options.pagination, sorting: options.sorting, }); } return await getChildDocumentsInLanguage(siteContext, { parentNodeId: options.parentNodeId, language: options.language, typeNames, parentDocumentType, pagination: options.pagination, sorting: options.sorting, }); } async function getChildDocumentsInLanguage(siteContext, options) { const { cn } = siteContext; const { parentNodeId, language, typeNames, parentDocumentType, pagination, sorting } = options; const query = cn("PaLNode as l") .innerJoin("PaNode as n", "n.id", "l.nodeId") .leftJoin("PaDocument as d", function () { this.on("d.nodeId", "l.nodeId").andOn("d.language", "l.language"); }) .where("n.parentId", parentNodeId) .whereIn("n.typeName", typeNames) .andWhere("l.language", language) .select([ "l.nodeId", "l.language", "l.ready", "l.updatedAt", "d.title", "d.slug", "d.metaDescription", "d.metaKeywords", "d.overrideLanguage", ]); applyRegularChildrenSortingOnQuery(siteContext, { query, parentDocumentType, leftJoinDocument: false, sorting, }); query.limit(pagination.limit).offset(pagination.offset); const rows = await query; const items = rows.map((row) => toDocumentSeed(ChildDocumentRowAT.assert(row))); const total = await countChildDocumentsInLanguage(siteContext, { parentNodeId, language, typeNames, currentCount: items.length, pagination, }); return { total, items }; } async function countChildDocumentsInLanguage(siteContext, options) { const { currentCount, pagination } = options; if (currentCount < pagination.limit) { return pagination.offset + currentCount; } const countRow = await siteContext .cn("PaLNode as l") .innerJoin("PaNode as n", "n.id", "l.nodeId") .where("n.parentId", options.parentNodeId) .whereIn("n.typeName", options.typeNames) .andWhere("l.language", options.language) .count("l.nodeId as cnt") .first(); return countRow ? ChildLNodesCountRowAT.assert(countRow).cnt : 0; } async function getChildDocumentsMissingLanguage(siteContext, options) { const { cn } = siteContext; const { parentNodeId, language, typeNames, parentDocumentType, pagination, sorting } = options; const query = cn("PaNode as n") .innerJoin("PaLNode as l", "l.nodeId", "n.id") .where("n.parentId", parentNodeId) .whereIn("n.typeName", typeNames) .whereNotExists(function () { this.select(cn.raw("1")) .from("PaLNode as l2") .where("l2.nodeId", cn.raw("n.id")) .andWhere("l2.language", language); }) .select(["n.id as nodeId"]) .groupBy("n.id"); applyRegularChildrenSortingOnQuery(siteContext, { query, parentDocumentType, leftJoinDocument: true, sorting, useAggregates: true, }); query.limit(pagination.limit).offset(pagination.offset); const nodeIdRows = await query; const nodeIds = nodeIdRows.map((row) => String(row.nodeId)); if (nodeIds.length === 0) { return { total: pagination.offset, items: [] }; } const lNodeRows = await fetchOneDocumentRowPerNodeId(siteContext, nodeIds); const lNodesByNodeId = new Map(); for (const row of lNodeRows) { if (!lNodesByNodeId.has(row.nodeId)) { lNodesByNodeId.set(row.nodeId, row); } } const items = nodeIds .map((nodeId) => lNodesByNodeId.get(nodeId)) .filter((row) => row !== undefined) .map(toDocumentSeed); const total = await countChildDocumentsMissingLanguage(siteContext, { parentNodeId, language, typeNames, currentCount: items.length, pagination, }); return { total, items }; } async function fetchOneDocumentRowPerNodeId(siteContext, nodeIds) { const rows = await siteContext .cn("PaLNode as l") .leftJoin("PaDocument as d", function () { this.on("d.nodeId", "l.nodeId").andOn("d.language", "l.language"); }) .select([ "l.nodeId", "l.language", "l.ready", "l.updatedAt", "d.title", "d.slug", "d.metaDescription", "d.metaKeywords", "d.overrideLanguage", ]) .whereIn("l.nodeId", nodeIds); return rows.map((row) => ChildDocumentRowAT.assert(row)); } async function countChildDocumentsMissingLanguage(siteContext, options) { const { currentCount, pagination, parentNodeId, language, typeNames } = options; if (currentCount < pagination.limit) { return pagination.offset + currentCount; } const { cn } = siteContext; const countRow = await cn("PaNode as n") .where("n.parentId", parentNodeId) .whereIn("n.typeName", typeNames) .whereNotExists(function () { this.select(cn.raw("1")) .from("PaLNode as l") .where("l.nodeId", cn.raw("n.id")) .andWhere("l.language", language); }) .count("n.id as cnt") .first(); return countRow ? ChildLNodesCountRowAT.assert(countRow).cnt : 0; } //# sourceMappingURL=lnode.queries.js.map