From 064b0d2bf8f657414450bb3c9e5ec7d11562dc8f Mon Sep 17 00:00:00 2001 From: Ming Liu <95666491+mlzoo@users.noreply.github.com> Date: Tue, 9 Sep 2025 23:32:21 +0800 Subject: [PATCH] incremental page tree updates --- server/models/pages.js | 258 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 248 insertions(+), 10 deletions(-) diff --git a/server/models/pages.js b/server/models/pages.js index bb5b6585..fb43a3f9 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -332,8 +332,11 @@ module.exports = class Page extends Model { // -> Render page to HTML await WIKI.models.pages.renderPage(page) - // -> Rebuild page tree - await WIKI.models.pages.rebuildTree() + // -> Update page tree incrementally + await WIKI.models.pages.updatePageTreeIncremental({ + action: 'create', + page: page + }) // -> Add to Search Index const pageContents = await WIKI.models.pages.query().findById(page.id).select('render') @@ -476,10 +479,11 @@ module.exports = class Page extends Model { user: opts.user }) } else { - // -> Update title of page tree entry - await WIKI.models.knex.table('pageTree').where({ - pageId: page.id - }).update('title', page.title) + // -> Update page tree incrementally + await WIKI.models.pages.updatePageTreeIncremental({ + action: 'update', + page: page + }) } // -> Get latest updatedAt @@ -735,8 +739,18 @@ module.exports = class Page extends Model { await WIKI.models.pages.deletePageFromCache(page.hash) WIKI.events.outbound.emit('deletePageFromCache', page.hash) - // -> Rebuild page tree - await WIKI.models.pages.rebuildTree() + // -> Update page tree incrementally + await WIKI.models.pages.updatePageTreeIncremental({ + action: 'move', + oldPage: page, + page: { + ...page, + path: opts.destinationPath, + localeCode: opts.destinationLocale, + title: destinationTitle, + hash: destinationHash + } + }) // -> Rename in Search Index const pageContents = await WIKI.models.pages.query().findById(page.id).select('render') @@ -814,8 +828,11 @@ module.exports = class Page extends Model { await WIKI.models.pages.deletePageFromCache(page.hash) WIKI.events.outbound.emit('deletePageFromCache', page.hash) - // -> Rebuild page tree - await WIKI.models.pages.rebuildTree() + // -> Update page tree incrementally + await WIKI.models.pages.updatePageTreeIncremental({ + action: 'delete', + page: page + }) // -> Delete from Search Index await WIKI.data.searchEngine.deleted(page) @@ -928,6 +945,227 @@ module.exports = class Page extends Model { return rebuildJob.finished } + /** + * Incrementally update page tree for a specific page + * + * @param {Object} opts Page properties + * @param {string} opts.action Action type: 'create', 'update', 'delete', 'move' + * @param {Object} opts.page Page data + * @param {Object} [opts.oldPage] Original page data (for update/move operations) + * @returns {Promise} Promise with no value + */ + static async updatePageTreeIncremental(opts) { + try { + switch (opts.action) { + case 'create': + await WIKI.models.pages.addPageToTree(opts.page) + break + case 'update': + await WIKI.models.pages.updatePageInTree(opts.page) + break + case 'delete': + await WIKI.models.pages.removePageFromTree(opts.page) + break + case 'move': + await WIKI.models.pages.movePageInTree(opts.oldPage, opts.page) + break + default: + WIKI.logger.warn(`Unknown page tree action: ${opts.action}`) + } + } catch (err) { + WIKI.logger.error(`Failed to update page tree incrementally: ${err.message}`) + // Fallback to full rebuild if incremental update fails + await WIKI.models.pages.rebuildTree() + } + } + + /** + * Add a new page to the page tree + * + * @param {Object} page Page data + * @returns {Promise} Promise with no value + */ + static async addPageToTree(page) { + const pagePaths = page.path.split('/') + let currentPath = '' + let depth = 0 + let parentId = null + let ancestors = [] + + for (const part of pagePaths) { + depth++ + const isFolder = (depth < pagePaths.length) + currentPath = currentPath ? `${currentPath}/${part}` : part + + // Check if this path already exists in the tree + const existing = await WIKI.models.knex.table('pageTree') + .where({ + localeCode: page.localeCode, + path: currentPath + }) + .first() + + if (!existing) { + // Create new tree entry + const newEntry = { + localeCode: page.localeCode, + path: currentPath, + depth: depth, + title: isFolder ? part : page.title, + isFolder: isFolder, + isPrivate: !isFolder && page.isPrivate, + privateNS: !isFolder ? page.privateNS : null, + parent: parentId, + pageId: isFolder ? null : page.id, + ancestors: JSON.stringify(ancestors) + } + + // Insert and reliably retrieve the auto-generated id (PostgreSQL) + let inserted = await WIKI.models.knex + .table('pageTree') + .insert(newEntry) + .returning('id') + + // Knex returns an array; depending on driver it can be an array of ids or objects + const insertedId = Array.isArray(inserted) + ? (inserted[0] && (inserted[0].id || inserted[0])) + : inserted + parentId = insertedId + } else { + // 如果现有节点是页面但需要变成文件夹,则更新 + if (isFolder && !existing.isFolder) { + await WIKI.models.knex.table('pageTree') + .where('id', existing.id) + .update({ + isFolder: true + // 注意:不清空pageId,保持页面内容的关联 + }) + } + parentId = existing.id + } + ancestors.push(parentId) + } + + // Update ancestors for all child entries + await WIKI.models.pages.updateAncestorsForChildren(parentId, ancestors) + } + + /** + * Update a page in the page tree + * + * @param {Object} page Page data + * @returns {Promise} Promise with no value + */ + static async updatePageInTree(page) { + // Update the page title in the tree + await WIKI.models.knex.table('pageTree') + .where({ + pageId: page.id, + localeCode: page.localeCode + }) + .update({ + title: page.title, + isPrivate: page.isPrivate, + privateNS: page.privateNS + }) + } + + /** + * Remove a page from the page tree + * + * @param {Object} page Page data + * @returns {Promise} Promise with no value + */ + static async removePageFromTree(page) { + // Find the tree entry for this page + const treeEntry = await WIKI.models.knex.table('pageTree') + .where({ + pageId: page.id, + localeCode: page.localeCode + }) + .first() + + if (!treeEntry) return + + // Remove the page entry + await WIKI.models.knex.table('pageTree') + .where('id', treeEntry.id) + .del() + + // Check if parent folders are now empty and can be removed + await WIKI.models.pages.cleanupEmptyFolders(treeEntry.parent) + } + + /** + * Move a page in the page tree + * + * @param {Object} oldPage Original page data + * @param {Object} newPage New page data + * @returns {Promise} Promise with no value + */ + static async movePageInTree(oldPage, newPage) { + // Remove from old location + await WIKI.models.pages.removePageFromTree(oldPage) + + // Add to new location + await WIKI.models.pages.addPageToTree(newPage) + } + + /** + * Update ancestors for child entries + * + * @param {number} parentId Parent ID + * @param {Array} ancestors Ancestors array + * @returns {Promise} Promise with no value + */ + static async updateAncestorsForChildren(parentId, ancestors) { + const children = await WIKI.models.knex.table('pageTree') + .where('parent', parentId) + .select('id') + + for (const child of children) { + const childAncestors = [...ancestors, child.id] + await WIKI.models.knex.table('pageTree') + .where('id', child.id) + .update('ancestors', JSON.stringify(childAncestors)) + + // Recursively update children + await WIKI.models.pages.updateAncestorsForChildren(child.id, childAncestors) + } + } + + /** + * Clean up empty folders in the page tree + * + * @param {number} parentId Parent ID to check + * @returns {Promise} Promise with no value + */ + static async cleanupEmptyFolders(parentId) { + if (!parentId) return + + const parent = await WIKI.models.knex.table('pageTree') + .where('id', parentId) + .first() + + if (!parent || !parent.isFolder) return + + // Check if this folder has any children + const children = await WIKI.models.knex.table('pageTree') + .where('parent', parentId) + .count('* as count') + .first() + + if (children.count === 0) { + // Remove empty folder + await WIKI.models.knex.table('pageTree') + .where('id', parentId) + .del() + + // Check parent folder + await WIKI.models.pages.cleanupEmptyFolders(parent.parent) + } + } + /** * Trigger the rendering of a page *