Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/ISSUE_TEMPLATE/BUG.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ body:
- child-process-manager
- confluence-sync
- markdown-confluence-sync
default: 0
default: 2
validations:
required: true
- type: textarea
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

6 changes: 6 additions & 0 deletions components/confluence-sync/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions components/confluence-sync/mocks/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
80 changes: 79 additions & 1 deletion components/confluence-sync/mocks/routes/Confluence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion components/confluence-sync/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
{
Expand All @@ -47,25 +50,85 @@ export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomC
return this._logger;
}

private async _getChildPages(
parentId: ConfluenceId,
start: number = 0,
otherChildren: Models.Content[] = [],
): Promise<Models.Content[]> {
try {
this._logger.silly(`Getting child pages of parent with id ${parentId}`);
const response = await axios.get<Models.ContentChildren>(
`${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<ConfluencePage> {
try {
this._logger.silly(`Getting page with id ${id}`);
const response: Models.Content =
await this._client.content.getContentById({

const childrenRequest: Promise<Models.Content[]> =
this._getChildPages(id);

const pageRequest: Promise<Models.Content> =
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,
version: response.version?.number as number,
ancestors: response.ancestors?.map((ancestor) =>
this._convertToConfluencePageBasicInfo(ancestor),
),
children: response.children?.page?.results?.map((child) =>
children: childrenResponse.map((child) =>
this._convertToConfluencePageBasicInfo(child),
),
};
Expand Down
Loading
Loading