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