diff --git a/.github/ISSUE_TEMPLATE/BUG.yml b/.github/ISSUE_TEMPLATE/BUG.yml index 212c9964..a44f0238 100644 --- a/.github/ISSUE_TEMPLATE/BUG.yml +++ b/.github/ISSUE_TEMPLATE/BUG.yml @@ -23,7 +23,7 @@ body: - child-process-manager - confluence-sync - markdown-confluence-sync - default: 0 + default: 2 validations: required: true - type: textarea @@ -41,8 +41,9 @@ body: label: Version description: What version are you using? options: - - 1.x (Default) - default: 0 + - 1.x + - 2.x + default: 1 validations: required: true - type: textarea diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9d204845..82868bd5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,4 +21,3 @@ jobs: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc echo "@telefonica:registry=https://registry.npmjs.org/" >> ~/.npmrc pnpm -r publish --no-git-checks - diff --git a/components/confluence-sync/CHANGELOG.md b/components/confluence-sync/CHANGELOG.md index bd79c83a..dbd32f06 100644 --- a/components/confluence-sync/CHANGELOG.md +++ b/components/confluence-sync/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Deprecated #### Removed +## [2.0.2] - 2025-07-11 + +### Fixed + +* fix: Fix issue when a page has more than 25 children. There was an error when trying to update a children page that had 25 brothers. The API was not returning it as a child of the parent page, so the library was trying to create it as a new page instead of updating it. Now, we call directly to the Confluence API paginated and recursively to get all children pages, so we can update them correctly. + ## [2.0.1] - 2025-04-15 ### Changed diff --git a/components/confluence-sync/mocks/collections.ts b/components/confluence-sync/mocks/collections.ts index 3cc3039a..98ecc7b8 100644 --- a/components/confluence-sync/mocks/collections.ts +++ b/components/confluence-sync/mocks/collections.ts @@ -13,6 +13,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:empty-root", + "confluence-get-page-children:empty-root", "confluence-create-page:empty-root", // "confluence-update-page:success", // "confluence-delete-page:success", @@ -23,6 +24,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:default-root", + "confluence-get-page-children:default-root", "confluence-create-page:default-root", "confluence-update-page:default-root", "confluence-delete-page:default-root", @@ -35,6 +37,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:hierarchical-empty-root", + "confluence-get-page-children:hierarchical-empty-root", "confluence-create-page:hierarchical-empty-root", // "confluence-update-page:hierarchical-empty-root", // "confluence-delete-page:hierarchical-empty-root", @@ -45,6 +48,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:hierarchical-default-root", + "confluence-get-page-children:hierarchical-default-root", "confluence-create-page:hierarchical-default-root", "confluence-update-page:hierarchical-default-root", "confluence-delete-page:hierarchical-default-root", @@ -55,6 +59,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:flat-mode", + "confluence-get-page-children:flat-mode", "confluence-create-page:flat-mode", "confluence-update-page:flat-mode", "confluence-delete-page:flat-mode", @@ -66,6 +71,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:renamed-page", + "confluence-get-page-children:renamed-page", "confluence-create-page:renamed-page", "confluence-delete-page:renamed-page", "confluence-get-attachments:renamed-page", diff --git a/components/confluence-sync/mocks/routes/Confluence.ts b/components/confluence-sync/mocks/routes/Confluence.ts index 2dd5401d..73ddb86e 100644 --- a/components/confluence-sync/mocks/routes/Confluence.ts +++ b/components/confluence-sync/mocks/routes/Confluence.ts @@ -53,7 +53,6 @@ function getPageMiddleware(pages) { content: "", version: { number: 1 }, ancestors: page.ancestors, - children: page.children, }; core.logger.info(`Sending page ${JSON.stringify(pageData)}`); res.status(200).json(pageData); @@ -66,6 +65,34 @@ function getPageMiddleware(pages) { }; } +function getPageChildrenMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Requested page with id ${req.params.pageId} to Confluence`, + ); + + addRequest("confluence-get-page-children", req); + const page = pages.find( + (pageCandidate) => pageCandidate.id === req.params.pageId, + ); + if (page) { + const pageData = page.children; + core.logger.info(`Sending page children ${JSON.stringify(pageData)}`); + res.status(200).json(pageData); + } else { + core.logger.error( + `Page with id ${req.params.pageId} not found in Confluence`, + ); + res.status(404).send(); + } + }; +} + function createPageMiddleware(pages) { return ( req: ServerRequest, @@ -272,6 +299,57 @@ const confluenceRoutes: RouteDefinition[] = [ }, ], }, + { + id: "confluence-get-page-children", + url: "/rest/api/content/:pageId/child", + method: "GET", + variants: [ + { + id: "empty-root", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_EMPTY_ROOT), + }, + }, + { + id: "default-root", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_DEFAULT_ROOT_GET), + }, + }, + { + id: "hierarchical-empty-root", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_EMPTY_ROOT_HIERARCHICAL), + }, + }, + { + id: "hierarchical-default-root", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware( + PAGES_DEFAULT_ROOT_GET_HIERARCHICAL, + ), + }, + }, + { + id: "flat-mode", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_FLAT_MODE), + }, + }, + { + id: "renamed-page", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(RENAMED_PAGE), + }, + }, + ], + }, { id: "confluence-create-page", url: "/rest/api/content", diff --git a/components/confluence-sync/package.json b/components/confluence-sync/package.json index 5665989c..bede107a 100644 --- a/components/confluence-sync/package.json +++ b/components/confluence-sync/package.json @@ -1,7 +1,7 @@ { "name": "@telefonica/confluence-sync", "description": "Creates/updates/deletes Confluence pages based on a list of objects containing the page contents. Supports nested pages and attachments upload", - "version": "2.0.1", + "version": "2.0.2", "license": "Apache-2.0", "author": "Telefónica Innovación Digital", "repository": { diff --git a/components/confluence-sync/src/confluence/CustomConfluenceClient.ts b/components/confluence-sync/src/confluence/CustomConfluenceClient.ts index 53554b07..30d920ef 100644 --- a/components/confluence-sync/src/confluence/CustomConfluenceClient.ts +++ b/components/confluence-sync/src/confluence/CustomConfluenceClient.ts @@ -1,9 +1,10 @@ -// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital +// SPDX-FileCopyrightText: 2025 Telefónica Innovación Digital // SPDX-License-Identifier: Apache-2.0 import type { LoggerInterface } from "@mocks-server/logger"; import type { Models } from "confluence.js"; import { ConfluenceClient } from "confluence.js"; +import axios from "axios"; import type { Attachments, @@ -23,6 +24,8 @@ import { DeletePageError } from "./errors/DeletePageError"; import { PageNotFoundError } from "./errors/PageNotFoundError"; import { UpdatePageError } from "./errors/UpdatePageError"; +const GET_CHILDREN_LIMIT = 100; + export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomConfluenceClient implements ConfluenceClientInterface { @@ -47,17 +50,77 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC return this._logger; } + private async _getChildPages( + parentId: ConfluenceId, + start: number = 0, + otherChildren: Models.Content[] = [], + ): Promise { + try { + this._logger.silly(`Getting child pages of parent with id ${parentId}`); + const response = await axios.get( + `${this._config.url}/rest/api/content/${parentId}/child`, + { + params: { + start, + limit: GET_CHILDREN_LIMIT, + expand: "page", + }, + headers: { + accept: "application/json", + Authorization: `Bearer ${this._config.personalAccessToken}`, + }, + }, + ); + this._logger.silly( + `Get child pages response of page ${parentId}, starting at ${start}: ${JSON.stringify(response.data, null, 2)}`, + ); + + const childrenResults = response.data.page?.results || []; + const size = response.data.page?.size || 0; + + const allChildren: Models.Content[] = [ + ...otherChildren, + ...childrenResults, + ]; + + if (start + childrenResults.length < size) { + const newStart = start + GET_CHILDREN_LIMIT; + this._logger.silly( + `There are more child pages of page with id ${parentId}, fetching next page starting from ${newStart}`, + ); + return this._getChildPages(parentId, newStart, allChildren); + } + + return allChildren; + } catch (error) { + throw new PageNotFoundError(parentId, { cause: error }); + } + } + public async getPage(id: string): Promise { try { this._logger.silly(`Getting page with id ${id}`); - const response: Models.Content = - await this._client.content.getContentById({ + + const childrenRequest: Promise = + this._getChildPages(id); + + const pageRequest: Promise = + this._client.content.getContentById({ id, - expand: ["ancestors", "version.number", "children.page"], + expand: ["ancestors", "version.number"], }); + + const [response, childrenResponse] = await Promise.all([ + pageRequest, + childrenRequest, + ]); + this._logger.silly( `Get page response: ${JSON.stringify(response, null, 2)}`, ); + this._logger.silly( + `Get children response: ${JSON.stringify(childrenResponse, null, 2)}`, + ); return { title: response.title, id: response.id, @@ -65,7 +128,7 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC ancestors: response.ancestors?.map((ancestor) => this._convertToConfluencePageBasicInfo(ancestor), ), - children: response.children?.page?.results?.map((child) => + children: childrenResponse.map((child) => this._convertToConfluencePageBasicInfo(child), ), }; diff --git a/components/confluence-sync/test/component/specs/Sync.spec.ts b/components/confluence-sync/test/component/specs/Sync.spec.ts index f656b4c4..8fb12a72 100644 --- a/components/confluence-sync/test/component/specs/Sync.spec.ts +++ b/components/confluence-sync/test/component/specs/Sync.spec.ts @@ -1,3 +1,4 @@ +/*eslint-disable jest/max-expects */ // SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital // SPDX-License-Identifier: Apache-2.0 @@ -504,6 +505,7 @@ describe("confluence-sync-pages library", () => { describe("when a page has been renamed", () => { let requests: SpyRequest[]; let getPageRequests: SpyRequest[]; + let getPageChildrenRequests: SpyRequest[]; beforeAll(async () => { await changeMockCollection("renamed-page"); @@ -511,49 +513,83 @@ describe("confluence-sync-pages library", () => { requests = await getRequests(); getPageRequests = await getRequestsByRouteId("confluence-get-page"); + getPageChildrenRequests = await getRequestsByRouteId( + "confluence-get-page-children", + ); createRequests = await getRequestsByRouteId("confluence-create-page"); deleteRequests = await getRequestsByRouteId("confluence-delete-page"); }); it("should execute delete requests before create requests", async () => { - // First request is the one to get the root page - expect(requests[0].routeId).toBe("confluence-get-page"); - expect(getPageRequests[0].params?.pageId).toBe("foo-root-id"); - // Second request is the one to get the parent page, child of the root page + // Request to get the root page children + expect(requests[0].routeId).toBe("confluence-get-page-children"); + expect(getPageChildrenRequests[0].params?.pageId).toBe("foo-root-id"); + // Request to get the root page expect(requests[1].routeId).toBe("confluence-get-page"); + expect(getPageRequests[0].params?.pageId).toBe("foo-root-id"); + + // Request to get the root page children + expect(requests[2].routeId).toBe("confluence-get-page-children"); + expect(getPageChildrenRequests[1].params?.pageId).toBe( + "foo-parent-id", + ); + // Request to get the parent page, child of the root page + expect(requests[3].routeId).toBe("confluence-get-page"); expect(getPageRequests[1].params?.pageId).toBe("foo-parent-id"); - // Third request has to be the one to delete the parent page - expect(requests[2].routeId).toBe("confluence-delete-page"); + + // Request to delete the parent page + expect(requests[4].routeId).toBe("confluence-delete-page"); expect(deleteRequests[0].params?.pageId).toBe("foo-parent-id"); - // Fourth request has to be the one to create the renamed page because is child of the root page - expect(requests[3].routeId).toBe("confluence-create-page"); + // Request to create the renamed page because is child of the root page + expect(requests[5].routeId).toBe("confluence-create-page"); expect(createRequests[0].body?.title).toBe("foo-renamed-title"); - // Fifth request has to be the one to get the child1 page which is child of the parent page - expect(requests[4].routeId).toBe("confluence-get-page"); + + // Request to get the child1 page which is child of the parent page + expect(requests[6].routeId).toBe("confluence-get-page-children"); + expect(getPageChildrenRequests[2].params?.pageId).toBe( + "foo-child1-id", + ); + // Request to get the child1 page which is child of the parent page + expect(requests[7].routeId).toBe("confluence-get-page"); expect(getPageRequests[2].params?.pageId).toBe("foo-child1-id"); - // Sixth request has to be the one to delete the child1 page - expect(requests[5].routeId).toBe("confluence-delete-page"); + + // Request to delete the child1 page + expect(requests[8].routeId).toBe("confluence-delete-page"); expect(deleteRequests[1].params?.pageId).toBe("foo-child1-id"); - // Seventh request has to be the one to create the child1 page because is child of the renamed page - expect(requests[6].routeId).toBe("confluence-create-page"); + // Request to create the child1 page because is child of the renamed page + expect(requests[9].routeId).toBe("confluence-create-page"); expect(createRequests[1].body?.title).toBe("foo-child1-title"); - // Eighth request has to be the one to get the grandChild1 page which is child of the child1 page child of parent page - expect(requests[7].routeId).toBe("confluence-get-page"); + + // Request to get the grandChild1 page which is child of the child1 page child of parent page + expect(requests[10].routeId).toBe("confluence-get-page-children"); + expect(getPageChildrenRequests[3].params?.pageId).toBe( + "foo-grandChild1-id", + ); + // Request to get the grandChild1 page which is child of the child1 page child of parent page + expect(requests[11].routeId).toBe("confluence-get-page"); expect(getPageRequests[3].params?.pageId).toBe("foo-grandChild1-id"); - // Ninth request has to be the one to delete the grandChild1 page - expect(requests[8].routeId).toBe("confluence-delete-page"); + + // Request to delete the grandChild1 page + expect(requests[12].routeId).toBe("confluence-delete-page"); expect(deleteRequests[2].params?.pageId).toBe("foo-grandChild1-id"); - // Tenth request has to be the one to get the grandChild2 page because is child of the child1 page child of parent page - expect(requests[9].routeId).toBe("confluence-get-page"); + + // Request to get the grandChild2 page children because is child of the child1 page child of parent page + expect(requests[13].routeId).toBe("confluence-get-page-children"); + expect(getPageChildrenRequests[4].params?.pageId).toBe( + "foo-grandChild2-id", + ); + // Request to get the grandChild2 page because is child of the child1 page child of parent page + expect(requests[14].routeId).toBe("confluence-get-page"); expect(getPageRequests[4].params?.pageId).toBe("foo-grandChild2-id"); - // Eleventh request has to be the one to delete the grandChild2 page - expect(requests[10].routeId).toBe("confluence-delete-page"); + + // Request to delete the grandChild2 page + expect(requests[15].routeId).toBe("confluence-delete-page"); expect(deleteRequests[3].params?.pageId).toBe("foo-grandChild2-id"); - // Twelfth request has to be the one to create the grandChild1 page because is child of the child1 page child of the renamed page - expect(requests[11].routeId).toBe("confluence-create-page"); + // Request to create the grandChild1 page because is child of the child1 page child of the renamed page + expect(requests[16].routeId).toBe("confluence-create-page"); expect(createRequests[2].body?.title).toBe("foo-grandChild1-title"); - // Thirteenth request has to be the one to create the grandChild2 page because is child of the child1 page child of the renamed page - expect(requests[12].routeId).toBe("confluence-create-page"); + // Request to create the grandChild2 page because is child of the child1 page child of the renamed page + expect(requests[17].routeId).toBe("confluence-create-page"); expect(createRequests[3].body?.title).toBe("foo-grandChild2-title"); }); diff --git a/components/confluence-sync/test/unit/specs/confluence/CustomConfluenceClient.test.ts b/components/confluence-sync/test/unit/specs/confluence/CustomConfluenceClient.test.ts index 76d28ae9..81d462fb 100644 --- a/components/confluence-sync/test/unit/specs/confluence/CustomConfluenceClient.test.ts +++ b/components/confluence-sync/test/unit/specs/confluence/CustomConfluenceClient.test.ts @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital +// SPDX-FileCopyrightText: 2025 Telefónica Innovación Digital // SPDX-License-Identifier: Apache-2.0 import type { LoggerInterface } from "@mocks-server/logger"; import { Logger } from "@mocks-server/logger"; import type { AxiosResponse } from "axios"; -import { AxiosError } from "axios"; +import axios, { AxiosError } from "axios"; import type { Models } from "confluence.js"; import { cleanLogs } from "@support/Logs"; @@ -18,6 +18,12 @@ import type { ConfluencePage, } from "@src/index"; +// eslint-disable-next-line jest/no-untyped-mock-factory +jest.mock("axios", () => ({ + ...jest.requireActual("axios"), + get: jest.fn(), +})); + describe("customConfluenceClient class", () => { let logger: LoggerInterface; let config: ConfluenceClientConfig; @@ -51,6 +57,17 @@ describe("customConfluenceClient class", () => { { id: "foo-id-ancestor", title: "foo-ancestor", type: "page" }, ] as Models.Content[], } as Models.Content; + + jest.spyOn(axios, "get").mockResolvedValue({ + data: { + page: { + results: [ + { id: "foo-child-1-id", title: "foo-child-1" }, + { id: "foo-child-2-id", title: "foo-child-2" }, + ], + }, + } as Models.ContentChildren, + } as AxiosResponse); }); describe("getPage method", () => { @@ -59,7 +76,7 @@ describe("customConfluenceClient class", () => { expect(confluenceClient.content.getContentById).toHaveBeenCalledWith({ id: "foo-id", - expand: ["ancestors", "version.number", "children.page"], + expand: ["ancestors", "version.number"], }); }); @@ -71,15 +88,8 @@ describe("customConfluenceClient class", () => { ancestors: [ { id: "foo-id-ancestor", title: "foo-ancestor", type: "page" }, ], - children: { - page: { - results: [ - { id: "foo-child-1-id", title: "foo-child-1" }, - { id: "foo-child-2-id", title: "foo-child-2" }, - ], - }, - }, })); + const response = await customConfluenceClient.getPage("foo-id"); expect(response).toEqual({ @@ -104,6 +114,63 @@ describe("customConfluenceClient class", () => { "Error getting page with id foo-id: foo-error", ); }); + + it("should throw a PageNotFoundError if axios.get throws an error when getting children", async () => { + jest + .spyOn(axios, "get") + .mockImplementation() + .mockRejectedValueOnce("foo-error"); + + await expect(customConfluenceClient.getPage("foo-id")).rejects.toThrow( + "Error getting page with id foo-id: foo-error", + ); + }); + + it("should call recursive getChildPages method to get all children of the page", async () => { + confluenceClient.content.getContentById.mockImplementation(() => ({ + title: "foo-title", + id: "foo-id", + version: { number: 1 }, + ancestors: [ + { id: "foo-id-ancestor", title: "foo-ancestor", type: "page" }, + ], + })); + jest.spyOn(axios, "get").mockResolvedValue({ + data: { + page: { + results: Array(100).fill({ + id: "foo-child-1-id", + title: "foo-child-1", + }), + size: 1000, + }, + } as Models.ContentChildren, + } as AxiosResponse); + + const response = await customConfluenceClient.getPage("foo-id"); + + expect(axios.get).toHaveBeenCalledTimes(10); + + expect(response.children).toHaveLength(1000); + }); + + it("should not fail if Confluence does not return results when requesting children", async () => { + confluenceClient.content.getContentById.mockImplementation(() => ({ + title: "foo-title", + id: "foo-id", + version: { number: 1 }, + ancestors: [ + { id: "foo-id-ancestor", title: "foo-ancestor", type: "page" }, + ], + })); + jest.spyOn(axios, "get").mockResolvedValue({ + data: {} as Models.ContentChildren, + } as AxiosResponse); + + expect( + async () => await customConfluenceClient.getPage("foo-id"), + ).not.toThrow(); + }); }); describe("createPage method", () => { diff --git a/components/markdown-confluence-sync/CHANGELOG.md b/components/markdown-confluence-sync/CHANGELOG.md index 45f1eb50..2cccfa11 100644 --- a/components/markdown-confluence-sync/CHANGELOG.md +++ b/components/markdown-confluence-sync/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Deprecated #### Removed +## [2.1.1] - 2025-07-11 + +### Fixed + +* fix: Fix issue when a page has more than 25 children. There was an error when trying to update a children page that had 25 brothers. The API was not returning it as a child of the parent page, so the library was trying to create it as a new page instead of updating it. Now, we use another API call to get all children pages. + + ## [2.1.0] - 2025-06-10 ### Added diff --git a/components/markdown-confluence-sync/mocks/collections.ts b/components/markdown-confluence-sync/mocks/collections.ts index cc890dec..d6de336b 100644 --- a/components/markdown-confluence-sync/mocks/collections.ts +++ b/components/markdown-confluence-sync/mocks/collections.ts @@ -13,6 +13,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:empty-root", + "confluence-get-page-children:empty-root", "confluence-create-page:empty-root", "confluence-create-attachments:empty-root", // "confluence-update-page:success", @@ -24,6 +25,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:default-root", + "confluence-get-page-children:default-root", "confluence-create-page:default-root", "confluence-update-page:default-root", "confluence-delete-page:default-root", @@ -36,6 +38,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:with-root-page-name", + "confluence-get-page-children:with-root-page-name", "confluence-create-page:with-root-page-name", ], }, @@ -44,6 +47,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:with-mdx-files", + "confluence-get-page-children:with-mdx-files", "confluence-create-page:with-mdx-files", ], }, @@ -52,6 +56,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:with-confluence-title", + "confluence-get-page-children:with-confluence-title", "confluence-create-page:with-confluence-title", ], }, @@ -60,6 +65,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:with-alternative-index-files", + "confluence-get-page-children:with-alternative-index-files", "confluence-create-page:with-alternative-index-files", ], }, @@ -68,6 +74,7 @@ const collection: CollectionDefinition[] = [ from: "base", routes: [ "confluence-get-page:with-confluence-page-id", + "confluence-get-page-children:with-confluence-page-id", "confluence-create-page:with-confluence-page-id", "confluence-update-page:with-confluence-page-id", "confluence-get-attachments:with-confluence-page-id", diff --git a/components/markdown-confluence-sync/mocks/routes/Confluence.ts b/components/markdown-confluence-sync/mocks/routes/Confluence.ts index 7741155f..e17bfdb0 100644 --- a/components/markdown-confluence-sync/mocks/routes/Confluence.ts +++ b/components/markdown-confluence-sync/mocks/routes/Confluence.ts @@ -47,7 +47,6 @@ function getPageMiddleware(pages) { content: "", version: { number: 1 }, ancestors: page.ancestors, - children: page.children, }; core.logger.info(`Sending page ${JSON.stringify(pageData)}`); res.status(200).json(pageData); @@ -60,6 +59,34 @@ function getPageMiddleware(pages) { }; } +function getPageChildrenMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Requested page children with id ${req.params.pageId} to Confluence`, + ); + + addRequest("confluence-get-page-children", req); + const page = pages.find( + (pageCandidate) => pageCandidate.id === req.params.pageId, + ); + if (page) { + const pageData = page.children; + core.logger.info(`Sending page ${JSON.stringify(pageData)}`); + res.status(200).json(pageData); + } else { + core.logger.error( + `Page with id ${req.params.pageId} not found in Confluence`, + ); + res.status(404).send(); + } + }; +} + function createPageMiddleware(pages) { return ( req: ServerRequest, @@ -273,6 +300,64 @@ const confluenceRoutes: RouteDefinition[] = [ }, ], }, + { + id: "confluence-get-page-children", + url: "/rest/api/content/:pageId/child", + method: "GET", + variants: [ + { + id: "empty-root", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_EMPTY_ROOT), + }, + }, + { + id: "default-root", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_DEFAULT_ROOT_GET), + }, + }, + { + id: "with-root-page-name", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_WITH_ROOT_PAGE_NAME), + }, + }, + { + id: "with-mdx-files", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_WITH_MDX_FILES), + }, + }, + { + id: "with-confluence-title", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_WITH_CONFLUENCE_TITLE), + }, + }, + { + id: "with-alternative-index-files", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware( + PAGES_WITH_ALTERNATIVE_INDEX_FILES, + ), + }, + }, + { + id: "with-confluence-page-id", + type: "middleware", + options: { + middleware: getPageChildrenMiddleware(PAGES_WITH_CONFLUENCE_PAGE_ID), + }, + }, + ], + }, { id: "confluence-create-page", url: "/rest/api/content", diff --git a/components/markdown-confluence-sync/package.json b/components/markdown-confluence-sync/package.json index ebb7df51..685701a8 100644 --- a/components/markdown-confluence-sync/package.json +++ b/components/markdown-confluence-sync/package.json @@ -1,7 +1,7 @@ { "name": "@telefonica/markdown-confluence-sync", "description": "Creates/updates/deletes Confluence pages based on markdown files in a directory. Supports Mermaid diagrams and per-page configuration using frontmatter metadata. Works great with Docusaurus", - "version": "2.1.0", + "version": "2.1.1", "license": "Apache-2.0", "author": "Telefónica Innovación Digital", "repository": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d47d2f1..68d80e6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 22.9.0 '@typescript-eslint/eslint-plugin': specifier: 8.14.0 - version: 8.14.0(@typescript-eslint/parser@8.14.0)(eslint@9.7.0)(typescript@5.6.3) + version: 8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3) '@typescript-eslint/parser': specifier: 8.14.0 version: 8.14.0(eslint@9.7.0)(typescript@5.6.3) @@ -55,19 +55,19 @@ importers: version: 9.1.0(eslint@9.7.0) eslint-import-resolver-alias: specifier: 1.1.2 - version: 1.1.2(eslint-plugin-import@2.31.0) + version: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0)) eslint-import-resolver-typescript: specifier: 3.6.3 - version: 3.6.3(@typescript-eslint/parser@8.14.0)(eslint-plugin-import@2.31.0)(eslint@9.7.0) + version: 3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0) eslint-plugin-import: specifier: 2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) + version: 2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) eslint-plugin-jest: specifier: 28.9.0 - version: 28.9.0(@typescript-eslint/eslint-plugin@8.14.0)(eslint@9.7.0)(jest@29.7.0)(typescript@5.6.3) + version: 28.9.0(@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3) eslint-plugin-prettier: specifier: 5.1.3 - version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@9.7.0)(prettier@3.5.3) + version: 5.1.3(eslint-config-prettier@9.1.0(eslint@9.7.0))(eslint@9.7.0)(prettier@3.5.3) globals: specifier: 15.12.0 version: 15.12.0 @@ -152,7 +152,7 @@ importers: dependencies: '@mermaid-js/mermaid-cli': specifier: 11.4.0 - version: 11.4.0(puppeteer@23.11.1) + version: 11.4.0(puppeteer@23.11.1(typescript@5.6.3)) '@mocks-server/config': specifier: 2.0.0-beta.3 version: 2.0.0-beta.3 @@ -1993,11 +1993,15 @@ packages: atlassian-jwt@2.0.3: resolution: {integrity: sha512-G9oO3HHS1UKgsLRXj6nNKv2TY6g3PleBCdzHwbFeVKg+18GBFIMRz+ApxuOuWAgcL7RngNFF5rGNtw1Ss3hvTg==} engines: {node: '>= 0.4.0'} + deprecated: 'DEPRECATED: atlassian-jwt has moved to @atlassian/atlassian-jwt. The latest version is 2.1.1. Please update your dependency.' available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + axios@1.6.7: resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} @@ -7439,7 +7443,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@mermaid-js/mermaid-cli@11.4.0(puppeteer@23.11.1)': + '@mermaid-js/mermaid-cli@11.4.0(puppeteer@23.11.1(typescript@5.6.3))': dependencies: chalk: 5.4.1 commander: 12.1.0 @@ -7929,7 +7933,7 @@ snapshots: '@types/node': 22.9.0 optional: true - '@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0)(eslint@9.7.0)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 '@typescript-eslint/parser': 8.14.0(eslint@9.7.0)(typescript@5.6.3) @@ -7942,6 +7946,7 @@ snapshots: ignore: 5.3.2 natural-compare: 1.4.0 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -7954,6 +7959,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.14.0 debug: 4.4.0 eslint: 9.7.0 + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -7974,6 +7980,7 @@ snapshots: '@typescript-eslint/utils': 8.14.0(eslint@9.7.0)(typescript@5.6.3) debug: 4.4.0 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - eslint @@ -7993,6 +8000,7 @@ snapshots: minimatch: 9.0.5 semver: 7.7.1 ts-api-utils: 1.4.3(typescript@5.6.3) + optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -8193,6 +8201,14 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axios@1.10.0: + dependencies: + follow-redirects: 1.15.9(debug@4.3.7) + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.6.7: dependencies: follow-redirects: 1.15.9(debug@4.3.7) @@ -8328,8 +8344,9 @@ snapshots: bare-stream@2.6.5(bare-events@2.5.4): dependencies: - bare-events: 2.5.4 streamx: 2.22.0 + optionalDependencies: + bare-events: 2.5.4 optional: true base64-js@1.5.1: {} @@ -8634,7 +8651,7 @@ snapshots: confluence.js@1.7.4: dependencies: atlassian-jwt: 2.0.3 - axios: 1.8.4(debug@4.3.7) + axios: 1.10.0 form-data: 4.0.2 oauth: 0.10.2 tslib: 2.8.1 @@ -8690,6 +8707,7 @@ snapshots: import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 + optionalDependencies: typescript: 5.6.3 create-jest@29.7.0(@types/node@22.9.0): @@ -9299,9 +9317,9 @@ snapshots: dependencies: eslint: 9.7.0 - eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0): + eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0)): dependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) eslint-import-resolver-node@0.3.9: dependencies: @@ -9311,38 +9329,39 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0)(eslint-plugin-import@2.31.0)(eslint@9.7.0): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 enhanced-resolve: 5.18.1 eslint: 9.7.0 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0))(eslint@9.7.0) fast-glob: 3.3.3 get-tsconfig: 4.10.0 is-bun-module: 1.3.0 is-glob: 4.0.3 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0))(eslint@9.7.0): dependencies: - '@typescript-eslint/parser': 8.14.0(eslint@9.7.0)(typescript@5.6.3) debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.14.0(eslint@9.7.0)(typescript@5.6.3) eslint: 9.7.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.14.0)(eslint-plugin-import@2.31.0)(eslint@9.7.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0): dependencies: '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 8.14.0(eslint@9.7.0)(typescript@5.6.3) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 @@ -9351,7 +9370,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.7.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0))(eslint@9.7.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -9362,28 +9381,32 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.14.0(eslint@9.7.0)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@28.9.0(@typescript-eslint/eslint-plugin@8.14.0)(eslint@9.7.0)(jest@29.7.0)(typescript@5.6.3): + eslint-plugin-jest@28.9.0(@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.14.0(@typescript-eslint/parser@8.14.0)(eslint@9.7.0)(typescript@5.6.3) '@typescript-eslint/utils': 8.29.1(eslint@9.7.0)(typescript@5.6.3) eslint: 9.7.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3) jest: 29.7.0(@types/node@22.9.0) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@9.7.0)(prettier@3.5.3): + eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0(eslint@9.7.0))(eslint@9.7.0)(prettier@3.5.3): dependencies: eslint: 9.7.0 - eslint-config-prettier: 9.1.0(eslint@9.7.0) prettier: 3.5.3 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 + optionalDependencies: + eslint-config-prettier: 9.1.0(eslint@9.7.0) eslint-scope@8.3.0: dependencies: @@ -9615,7 +9638,7 @@ snapshots: pend: 1.2.0 fdir@6.4.3(picomatch@4.0.2): - dependencies: + optionalDependencies: picomatch: 4.0.2 fecha@4.2.3: {} @@ -9691,7 +9714,7 @@ snapshots: fn.name@1.1.0: {} follow-redirects@1.15.9(debug@4.3.7): - dependencies: + optionalDependencies: debug: 4.3.7 for-each@0.3.5: @@ -10490,7 +10513,6 @@ snapshots: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.9.0 babel-jest: 29.7.0(@babel/core@7.26.0) chalk: 4.1.2 ci-info: 3.9.0 @@ -10510,6 +10532,8 @@ snapshots: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.9.0 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -10591,7 +10615,7 @@ snapshots: jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - dependencies: + optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {}