Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
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,88 @@ 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: 100,
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) {
this._logger.silly(
`There are more child pages of page with id ${parentId}, fetching next page starting from ${start + 100}`,
);
return this._getChildPages(
parentId,
start + GET_CHILDREN_LIMIT,
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