Files
memind/mindspace-workspace-page-deliver.mjs

243 lines
8.8 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { slugFromPageTitle } from './mindspace-chat-plaza.mjs';
import { normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import { htmlUsesPageDataApi } from './page-data-html-detect.mjs';
import { resolvePublishDir } from './user-publish.mjs';
function parseJsonColumn(value, fallback = {}) {
if (value == null || value === '') return { ...fallback };
if (typeof value === 'object' && !Buffer.isBuffer(value)) return value;
try {
return JSON.parse(String(value));
} catch {
return { ...fallback };
}
}
function isPublicWorkspaceHtmlPath(relativePath) {
const normalized = normalizeWorkspaceRelativePath(relativePath);
return Boolean(normalized?.startsWith('public/') && normalized.toLowerCase().endsWith('.html'));
}
function readWorkspaceHtmlContent(publishDir, relativePath) {
const normalized = normalizeWorkspaceRelativePath(relativePath);
if (!normalized || !publishDir) return '';
const absolutePath = path.join(publishDir, ...normalized.split('/'));
try {
return fs.readFileSync(absolutePath, 'utf8');
} catch {
return '';
}
}
function isPageDataWorkspaceHtml(publishDir, relativePath) {
const content = readWorkspaceHtmlContent(publishDir, relativePath);
return htmlUsesPageDataApi(content);
}
function filterWorkspacePagesByRelativePaths(rows, onlyRelativePaths) {
if (onlyRelativePaths == null) return rows;
const allowed = new Set(
[...onlyRelativePaths]
.map((relativePath) => normalizeWorkspaceRelativePath(relativePath))
.filter(Boolean),
);
return rows.filter((row) => {
const snapshot = parseJsonColumn(row.source_snapshot_json);
const relativePath = normalizeWorkspaceRelativePath(
row.workspace_relative_path ?? snapshot.relative_path ?? null,
);
return relativePath ? allowed.has(relativePath) : false;
});
}
export function createWorkspacePageDeliverService({
pool,
pageService,
publicationService,
pageSyncService,
pageDataEnsure = null,
h5Root = null,
storageRoot = null,
findPageByRelativePath = null,
logger = console,
} = {}) {
async function listUnpublishedAutoSyncedWorkspacePages(userId, { onlyRelativePaths = null } = {}) {
if (!pool || !userId) return [];
const [rows] = await pool.query(
`SELECT p.id AS page_id, p.title, p.current_version_id, p.workspace_relative_path,
pv.source_snapshot_json
FROM h5_page_records p
JOIN h5_page_versions pv ON pv.id = p.current_version_id
LEFT JOIN h5_publish_records pr
ON pr.page_id = p.id AND pr.user_id = p.user_id AND pr.status = 'online'
WHERE p.user_id = ?
AND p.status <> 'deleted'
AND pr.id IS NULL`,
[userId],
);
const candidates = (rows ?? []).filter((row) => {
const snapshot = parseJsonColumn(row.source_snapshot_json);
const relativePath = row.workspace_relative_path ?? snapshot.relative_path ?? null;
if (!isPublicWorkspaceHtmlPath(relativePath)) return false;
return snapshot.auto_synced === true || snapshot.content_mode === 'static_html';
});
return filterWorkspacePagesByRelativePaths(candidates, onlyRelativePaths);
}
async function listOnlineAutoSyncedWorkspacePages(userId, { onlyRelativePaths = null } = {}) {
if (!pool || !userId) return [];
const [rows] = await pool.query(
`SELECT p.id AS page_id, p.title, p.current_version_id, p.workspace_relative_path,
pv.source_snapshot_json
FROM h5_page_records p
JOIN h5_page_versions pv ON pv.id = p.current_version_id
JOIN h5_publish_records pr
ON pr.page_id = p.id AND pr.user_id = p.user_id AND pr.status = 'online'
WHERE p.user_id = ?
AND p.status <> 'deleted'`,
[userId],
);
const candidates = (rows ?? []).filter((row) => {
const snapshot = parseJsonColumn(row.source_snapshot_json);
const relativePath = row.workspace_relative_path ?? snapshot.relative_path ?? null;
if (!isPublicWorkspaceHtmlPath(relativePath)) return false;
return snapshot.auto_synced === true || snapshot.content_mode === 'static_html';
});
return filterWorkspacePagesByRelativePaths(candidates, onlyRelativePaths);
}
function resolveWorkspaceRoot(userId) {
if (!h5Root || !userId) return null;
return resolvePublishDir(h5Root, { id: userId });
}
async function ensurePageDataBindings(userId, { onlyRelativePaths = null } = {}) {
if (!pageDataEnsure?.ensurePageDataHtmlPagesBound || !pool || !userId) {
return { bound: [], skipped: [], errors: [] };
}
const workspaceRoot = resolveWorkspaceRoot(userId);
if (!workspaceRoot) {
return { bound: [], skipped: [], errors: [{ code: 'missing_workspace_root' }] };
}
return pageDataEnsure.ensurePageDataHtmlPagesBound({
pool,
h5Root,
storageRoot,
userId,
workspaceRoot,
findPageByRelativePath,
onlyRelativePaths,
logger,
});
}
async function refreshOnlineWorkspacePublications(userId, { onlyRelativePaths = null } = {}) {
if (!publicationService?.refreshOnlinePublicationHtml || !userId) {
return { refreshed: 0, skipped: 0, errors: [] };
}
const candidates = await listOnlineAutoSyncedWorkspacePages(userId, { onlyRelativePaths });
let refreshed = 0;
let skipped = 0;
const errors = [];
for (const candidate of candidates) {
try {
const result = await publicationService.refreshOnlinePublicationHtml(
userId,
candidate.page_id,
);
if (result) refreshed += 1;
else skipped += 1;
} catch (error) {
errors.push({
pageId: candidate.page_id,
message: error instanceof Error ? error.message : String(error),
});
logger?.warn?.(
`[MindSpace] refresh publication failed for page ${candidate.page_id}:`,
error instanceof Error ? error.message : error,
);
}
}
return { refreshed, skipped, errors };
}
async function ensureWorkspaceHtmlPublications(userId, { onlyRelativePaths = null } = {}) {
if (!publicationService?.publish || !pageService?.getPage || !userId) {
return { published: 0, skipped: 0, errors: [] };
}
const candidates = await listUnpublishedAutoSyncedWorkspacePages(userId, { onlyRelativePaths });
const publishDir = resolveWorkspaceRoot(userId);
let published = 0;
let skipped = 0;
const errors = [];
for (const candidate of candidates) {
try {
const snapshot = parseJsonColumn(candidate.source_snapshot_json);
const relativePath =
candidate.workspace_relative_path ?? snapshot.relative_path ?? null;
if (isPageDataWorkspaceHtml(publishDir, relativePath)) {
skipped += 1;
continue;
}
const page = await pageService.getPage(userId, candidate.page_id);
const current = await publicationService.getCurrent?.(userId, candidate.page_id);
if (current?.status === 'online') {
skipped += 1;
continue;
}
await publicationService.publish(userId, candidate.page_id, {
pageVersionId: page.currentVersionId,
accessMode: 'public',
urlSlug: slugFromPageTitle(page.title, candidate.page_id),
autoAcknowledgeFindings: true,
});
published += 1;
} catch (error) {
errors.push({
pageId: candidate.page_id,
message: error instanceof Error ? error.message : String(error),
});
logger?.warn?.(
`[MindSpace] auto publish failed for page ${candidate.page_id}:`,
error instanceof Error ? error.message : error,
);
}
}
return { published, skipped, errors };
}
async function syncAndDeliver(userId, { pageDataRelativePaths = null } = {}) {
let syncResult = { created: 0, updated: 0, skipped: 0 };
if (pageSyncService?.syncUserGeneratedPages) {
syncResult = await pageSyncService.syncUserGeneratedPages(userId, {
onlyRelativePaths: pageDataRelativePaths,
});
}
const pageDataBindResult = await ensurePageDataBindings(userId, {
onlyRelativePaths: pageDataRelativePaths,
});
const publishResult = await ensureWorkspaceHtmlPublications(userId, {
onlyRelativePaths: pageDataRelativePaths,
});
const refreshResult = await refreshOnlineWorkspacePublications(userId, {
onlyRelativePaths: pageDataRelativePaths,
});
return {
pageDataRelativePaths: pageDataRelativePaths == null ? null : [...pageDataRelativePaths],
sync: syncResult,
pageDataBind: pageDataBindResult,
publish: publishResult,
refresh: refreshResult,
};
}
return {
syncAndDeliver,
ensurePageDataBindings,
ensureWorkspaceHtmlPublications,
refreshOnlineWorkspacePublications,
};
}