UNPKG

@telefonica/confluence-sync

Version:

Creates/updates/deletes Confluence pages based on a list of objects containing the page contents. Supports nested pages and attachments upload

318 lines (317 loc) 15.4 kB
"use strict"; // SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital // SPDX-License-Identifier: Apache-2.0 Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfluenceSyncPages = void 0; const promises_1 = require("node:fs/promises"); const logger_1 = require("@mocks-server/logger"); const fastq_1 = require("fastq"); const CustomConfluenceClient_1 = require("./confluence/CustomConfluenceClient"); const ConfluenceSyncPages_types_1 = require("./ConfluenceSyncPages.types"); const CompoundError_1 = require("./errors/CompoundError"); const NotAncestorsExpectedValidationError_1 = require("./errors/NotAncestorsExpectedValidationError"); const PendingPagesToSyncError_1 = require("./errors/PendingPagesToSyncError"); const RootPageRequiredException_1 = require("./errors/RootPageRequiredException"); const ShouldUseIdModeException_1 = require("./errors/ShouldUseIdModeException"); const RootPageForbiddenException_1 = require("./errors/RootPageForbiddenException"); const PagesWithoutIdException_1 = require("./errors/PagesWithoutIdException"); const InvalidSyncModeException_1 = require("./errors/InvalidSyncModeException"); const Pages_1 = require("./support/Pages"); const LOGGER_NAMESPACE = "confluence-sync-pages"; const DEFAULT_LOG_LEVEL = "silent"; const ConfluenceSyncPages = class ConfluenceSyncPages { _logger; _confluenceClient; _rootPageId; _syncMode; constructor({ logLevel, url, spaceId, personalAccessToken, dryRun, syncMode, rootPageId, }) { this._logger = new logger_1.Logger(LOGGER_NAMESPACE, { level: logLevel ?? DEFAULT_LOG_LEVEL, }); this._confluenceClient = new CustomConfluenceClient_1.CustomConfluenceClient({ url, spaceId, personalAccessToken, logger: this._logger.namespace("confluence"), dryRun, }); this._syncMode = syncMode ?? ConfluenceSyncPages_types_1.SyncModes.TREE; if (![ConfluenceSyncPages_types_1.SyncModes.FLAT, ConfluenceSyncPages_types_1.SyncModes.ID, ConfluenceSyncPages_types_1.SyncModes.TREE].includes(this._syncMode)) { throw new InvalidSyncModeException_1.InvalidSyncModeException(this._syncMode); } if (this._syncMode === ConfluenceSyncPages_types_1.SyncModes.TREE && rootPageId === undefined) { throw new RootPageRequiredException_1.RootPageRequiredException(ConfluenceSyncPages_types_1.SyncModes.TREE); } this._rootPageId = rootPageId; } get logger() { return this._logger; } async sync(pages) { const syncMode = this._syncMode; const confluencePages = new Map(); const pagesMap = new Map(pages.map((page) => [page.title, page])); const tasksDone = new Array(); const errors = new Array(); const queue = (0, fastq_1.promise)(this._handleTask.bind(this), 1); function enqueueTask(task) { queue.push({ task, enqueueTask, getConfluencePageByTitle, getPageByTitle, getPagesByAncestor, storeConfluencePage, }); } function getConfluencePageByTitle(pageTitle) { return confluencePages.get(pageTitle); } function storeConfluencePage(page) { confluencePages.set(page.title, page); } function getPageByTitle(pageTitle) { return pagesMap.get(pageTitle); } function getPagesByAncestor(ancestor, isRoot = false) { if (syncMode === ConfluenceSyncPages_types_1.SyncModes.FLAT) { // NOTE: in flat mode all pages without id are considered // children of root page. Otherwise, they are pages with mirror // page in Confluence and have no ancestors. if (isRoot) { return pages.filter((page) => page.id === undefined); } return []; } if (isRoot) { return pages .filter((page) => page.ancestors === undefined || page.ancestors.length === 0) .concat(pages.filter((page) => page.ancestors?.at(-1) === ancestor)); } return pages.filter((page) => page.ancestors?.at(-1) === ancestor); } queue.error((e, job) => { if (e) { errors.push(e); return; } const task = job.task; tasksDone.push(task); }); if (this._syncMode === ConfluenceSyncPages_types_1.SyncModes.FLAT) { this._validateFlatMode(pages); pages .filter((page) => page.id !== undefined) .forEach((page) => { enqueueTask({ type: "update", pageId: page.id, page }); }); } if (this._syncMode === ConfluenceSyncPages_types_1.SyncModes.ID) { this._validateIdMode(pages); pages.forEach((page) => { enqueueTask({ type: "update", pageId: page.id, page }); }); } if (this._rootPageId !== undefined) enqueueTask({ type: "init", pageId: this._rootPageId }); await queue.drained(); if (errors.length > 0) { const e = new CompoundError_1.CompoundError(...errors); this._logger.error(`Error occurs during sync: ${e}`); throw e; } this._assertPendingPagesToCreate(tasksDone, pages); this._reportTasks(tasksDone); } _validateIdMode(pages) { const pagesWithoutId = pages.filter((page) => page.id === undefined); if (pagesWithoutId.length > 0) { throw new PagesWithoutIdException_1.PagesWithoutIdException(pagesWithoutId); } if (this._rootPageId !== undefined) { throw new RootPageForbiddenException_1.RootPageForbiddenException(); } const pagesWithAncestors = pages.filter((page) => page.ancestors !== undefined && page.ancestors.length > 0); if (pagesWithAncestors.length > 0) { throw new NotAncestorsExpectedValidationError_1.NotAncestorsExpectedValidationError(pagesWithAncestors); } } _validateFlatMode(pages) { const pagesWithoutId = pages.filter((page) => page.id === undefined); if (pagesWithoutId.length === 0) { throw new ShouldUseIdModeException_1.ShouldUseIdModeException(`There are no pages without id. You should use ID mode instead of FLAT mode`); } if (this._rootPageId === undefined) { throw new RootPageRequiredException_1.RootPageRequiredException(ConfluenceSyncPages_types_1.SyncModes.FLAT); } const pagesWithAncestors = pages.filter((page) => page.ancestors !== undefined && page.ancestors.length > 0); if (pagesWithAncestors.length > 0) { throw new NotAncestorsExpectedValidationError_1.NotAncestorsExpectedValidationError(pagesWithAncestors); } } _assertPendingPagesToCreate(tasksDone, _pages) { const createdOrUpdatedPages = tasksDone .filter((task) => task.type === "create" || task.type === "update") .map((task) => task.page.title); const pendingPagesToCreate = _pages.filter((page) => !createdOrUpdatedPages.includes(page.title)); if (pendingPagesToCreate.length > 0) { const e = new PendingPagesToSyncError_1.PendingPagesToSyncError(pendingPagesToCreate); this._logger.error(e.message); throw e; } } _reportTasks(tasks) { const createdPages = tasks.filter((task) => task.type === "create"); this._logger.debug(`Created pages: ${createdPages.length} ${createdPages.map((task) => `+ ${task.page.title}`).join("\n")}`); const updatedPages = tasks.filter((task) => task.type === "update"); this._logger.debug(`Updated pages: ${updatedPages.length} ${updatedPages.map((task) => `⟳ #${task.pageId} ${task.page.title}`).join("\n")}`); const deletedPages = tasks.filter((task) => task.type === "delete"); this._logger.debug(`Deleted pages: ${deletedPages.length} ${deletedPages.map((task) => `- #${task.pageId}`).join("\n")}`); this._logger.info("Sync finished"); } _handleTask(job) { const task = job.task; switch (task.type) { case "init": return this._initSync({ ...job, task }); case "create": return this._createPage({ ...job, task }); case "update": return this._updatePage({ ...job, task }); case "delete": return this._deletePage({ ...job, task }); case "createAttachments": return this._createAttachments({ ...job, task }); case "deleteAttachments": return this._deleteAttachments({ ...job, task }); } } async _initSync(job) { this._logger.debug(`Reading page ${job.task.pageId}`); const confluencePage = await this._confluenceClient.getPage(job.task.pageId); job.storeConfluencePage(confluencePage); this._enqueueChildrenPages(job, confluencePage, true); } async _createPage(job) { this._logger.debug(`Creating page ${JSON.stringify(job.task.page)}`); const ancestors = job.task.page.ancestors?.map((ancestorTitle) => { const ancestor = job.getConfluencePageByTitle(ancestorTitle); // NOTE: This should never happen. Defensively check anyway. // istanbul ignore next if (ancestor === undefined) { throw new Error(`Could not find ancestor ${ancestorTitle}`); } return { id: ancestor.id, title: ancestor.title }; }); const confluencePage = await this._confluenceClient.createPage({ ...job.task.page, ancestors, }); job.storeConfluencePage(confluencePage); if (job.task.page.attachments !== undefined && Object.entries(job.task.page.attachments).length > 0) job.enqueueTask({ type: "createAttachments", pageId: confluencePage.id, pageTitle: confluencePage.title, attachments: job.task.page.attachments, }); const descendants = job.getPagesByAncestor(job.task.page.title); for (const descendant of descendants) { job.enqueueTask({ type: "create", page: descendant }); } } async _updatePage(job) { this._logger.debug(`Updating page ${job.task.page.title}`); const confluencePage = await this._confluenceClient.getPage(job.task.pageId); const updatedConfluencePage = await this._confluenceClient.updatePage({ id: confluencePage.id, title: job.task.page.title, content: job.task.page.content, ancestors: confluencePage.ancestors, version: confluencePage.version + 1, }); job.storeConfluencePage(updatedConfluencePage); const attachments = await this._confluenceClient.getAttachments(confluencePage.id); for (const attachment of attachments) { this._logger.debug(`Enqueueing delete attachment ${attachment.title} for page ${confluencePage.title}`); job.enqueueTask({ type: "deleteAttachments", pageId: attachment.id }); } if (job.task.page.attachments !== undefined && Object.entries(job.task.page.attachments).length > 0) { job.enqueueTask({ type: "createAttachments", pageId: confluencePage.id, pageTitle: confluencePage.title, attachments: job.task.page.attachments, }); } if (this._syncMode === ConfluenceSyncPages_types_1.SyncModes.TREE) { this._enqueueChildrenPages(job, confluencePage); } } async _deletePage(job) { this._logger.debug(`Deleting page ${job.task.pageId}`); const confluencePage = await this._confluenceClient.getPage(job.task.pageId); for (const descendant of confluencePage.children ?? []) { job.enqueueTask({ type: "delete", pageId: descendant.id }); } await this._confluenceClient.deleteContent(job.task.pageId); } async _deleteAttachments(job) { this._logger.debug(`Deleting attachment ${job.task.pageId}`); await this._confluenceClient.deleteContent(job.task.pageId); } async _createAttachments(job) { this._logger.debug(`Creating attachments for page ${job.task.pageTitle}, attachments: ${JSON.stringify(job.task.attachments)}`); const attachments = await Promise.all(Object.entries(job.task.attachments).map(async ([name, path]) => ({ filename: name, file: await (0, promises_1.readFile)(path), }))); await this._confluenceClient.createAttachments(job.task.pageId, attachments); } _enqueueChildrenPages(job, confluencePage, isRoot = false) { const descendants = job.getPagesByAncestor(confluencePage.title, isRoot); const confluenceDescendants = confluencePage.children ?? []; let descendantsWithPageId = []; if (isRoot) { descendants.forEach((descendant) => { descendant.ancestors = [confluencePage.title]; }); if (this._syncMode === ConfluenceSyncPages_types_1.SyncModes.FLAT) { const confluenceDescendantsInputPages = confluenceDescendants .map(({ title }) => job.getPageByTitle(title)) .filter(Boolean); descendantsWithPageId = (0, Pages_1.getPagesTitles)(confluenceDescendantsInputPages.filter((page) => page.id !== undefined)); if (descendantsWithPageId.length) this._logger.warn(`Some children of root page contains id: ${descendantsWithPageId.join(", ")}`); } } const descendantsToCreate = descendants.filter((descendant) => !confluenceDescendants.some((other) => other.title === descendant.title)); const descendantsToUpdate = confluenceDescendants .filter((descendant) => descendants.some((other) => other.title === descendant.title)) .map((descendant) => { const page = job.getPageByTitle(descendant.title); return { pageId: descendant.id, page }; }); const descendantsToDelete = confluenceDescendants.filter((descendant) => !descendants.some((other) => other.title === descendant.title) && !descendantsWithPageId.includes(descendant.title)); for (const descendant of descendantsToDelete) { job.enqueueTask({ type: "delete", pageId: descendant.id }); } for (const descendant of descendantsToCreate) { job.enqueueTask({ type: "create", page: descendant }); } for (const descendant of descendantsToUpdate) { job.enqueueTask({ type: "update", pageId: descendant.pageId, page: descendant.page, }); } } }; exports.ConfluenceSyncPages = ConfluenceSyncPages;