Skip to content

Commit ca91f51

Browse files
authored
feat: adding getEditURL support for content client (#1047)
1 parent e835c13 commit ca91f51

File tree

3 files changed

+227
-4
lines changed

3 files changed

+227
-4
lines changed

packages/spacecat-shared-content-client/src/clients/content-client.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,12 @@ export default class ContentClient {
311311
return docPath;
312312
}
313313

314-
async #getHelixResourceStatus(path) {
314+
async #getHelixResourceStatus(path, includeEditUrl = false) {
315315
const { rso } = this.site.getHlxConfig();
316-
// https://www.aem.live/docs/admin.html#tag/status
317-
const adminEndpointUrl = `https://admin.hlx.page/status/${rso.owner}/${rso.site}/${rso.ref}/${path.replace(/^\/+/, '')}`;
316+
// https://www.aem.live/docs/admin.html#tag/status,
317+
let adminEndpointUrl = `https://admin.hlx.page/status/${rso.owner}/${rso.site}/${rso.ref}/${path.replace(/^\/+/, '')}`;
318+
// ?editUrl=auto for URL of the edit (authoring) document
319+
adminEndpointUrl = includeEditUrl ? `${adminEndpointUrl}?editUrl=auto` : adminEndpointUrl;
318320
const response = await fetch(adminEndpointUrl, {
319321
headers: {
320322
Authorization: `token ${this.config.helixAdminToken}`,
@@ -349,6 +351,15 @@ export default class ContentClient {
349351
};
350352
}
351353

354+
/**
355+
* @param {string} path
356+
* @returns {Promise<string>}
357+
*/
358+
async getEditURL(path) {
359+
const helixResourceStatus = await this.#getHelixResourceStatus(path, true);
360+
return helixResourceStatus?.edit?.url;
361+
}
362+
352363
async getPageMetadata(path) {
353364
const startTime = process.hrtime.bigint();
354365

@@ -391,7 +402,7 @@ export default class ContentClient {
391402

392403
const response = await document.updateMetadata(mergedMetadata);
393404
if (response?.status !== 200) {
394-
throw new Error(`Failed to update metadata for path ${path}`);
405+
throw new Error(`Failed to update metadata for path ${path}: ${response.statusText}`);
395406
}
396407

397408
this.#logDuration('updatePageMetadata', startTime);

packages/spacecat-shared-content-client/src/clients/index.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,28 @@ export class ContentClient {
168168
getLivePreviewURLs(path: string):
169169
Promise<{ liveURL: string | undefined, previewURL: string | undefined }>;
170170

171+
/**
172+
* Retrieves the edit URL for a given content path from the AEM admin API.
173+
* The edit URL represents the URL of the document source for the given content path
174+
*
175+
* The path should stem from a page's URL and is relative to the site's root.
176+
* Example: "/path/to/page" (from the full URL: "https://www.example.com/path/to/page").
177+
*
178+
* @param {string} path The content path to get the edit URL for.
179+
*
180+
* @returns {Promise<string | undefined>} A promise that resolves to the edit URL
181+
* string if found, or undefined if not available.
182+
* @throws {Error} If the Helix admin API request fails or returns an error response.
183+
*
184+
* @example
185+
* ```typescript
186+
* const client = await ContentClient.createFrom(context, site);
187+
* const editURL = await client.getEditURL('/content/page');
188+
* console.log(editURL); // e.g., 'https://adobe.sharepoint.com/sites/Projects/_layouts/15/Doc.aspx?sourcedoc=%7xxxxxx-xxxx-xxxx-xxxx-xxxxxx%7D&file=page.docx&action=default'
189+
*
190+
*/
191+
getEditURL(path: string): Promise<string | undefined>;
192+
171193
/**
172194
* Retrieves all links from a document at the specified path.
173195
* This method extracts links from the document content, including both internal

packages/spacecat-shared-content-client/test/clients/content-client.test.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,196 @@ describe('ContentClient', () => {
967967
});
968968
});
969969

970+
describe('getEditURL', () => {
971+
let client;
972+
973+
function getHlxConfig() {
974+
return {
975+
...hlxConfigGoogle,
976+
rso: {
977+
owner: 'owner',
978+
site: 'repo',
979+
ref: 'main',
980+
},
981+
};
982+
}
983+
984+
const helixAdminToken = 'test-token';
985+
/** @type {SecretsManagerClient} */
986+
let secretsManagerClient;
987+
let sendStub;
988+
989+
beforeEach(async () => {
990+
secretsManagerClient = new SecretsManagerClient();
991+
sendStub = sinon.stub(secretsManagerClient, 'send');
992+
sendStub.resolves({ SecretString: JSON.stringify({ helix_admin_token: helixAdminToken }) });
993+
994+
client = await ContentClient.createFrom(
995+
context,
996+
{ ...siteConfigGoogleDrive, getHlxConfig },
997+
secretsManagerClient,
998+
);
999+
});
1000+
1001+
it('should return the edit URL on success', async () => {
1002+
const path = '/example/path';
1003+
const mockResponse = {
1004+
edit: { url: 'https://drive.google.com/document/d/abc123/edit' },
1005+
};
1006+
1007+
nock('https://admin.hlx.page', {
1008+
reqheaders: {
1009+
authorization: `token ${helixAdminToken}`,
1010+
},
1011+
})
1012+
.get('/status/owner/repo/main/example/path?editUrl=auto')
1013+
.reply(200, mockResponse);
1014+
1015+
expect(sendStub).to.have.been.calledWithMatch(
1016+
{
1017+
input: {
1018+
SecretId: resolveCustomerSecretsName(baseUrl, context),
1019+
},
1020+
},
1021+
);
1022+
const result = await client.getEditURL(path);
1023+
1024+
expect(result).to.equal('https://drive.google.com/document/d/abc123/edit');
1025+
});
1026+
1027+
it('should handle undefined edit URL in response', async () => {
1028+
const path = '/example-path';
1029+
const mockResponse = {
1030+
edit: null,
1031+
};
1032+
1033+
nock('https://admin.hlx.page')
1034+
.get('/status/owner/repo/main/example-path?editUrl=auto')
1035+
.reply(200, mockResponse);
1036+
1037+
const result = await client.getEditURL(path);
1038+
1039+
expect(result).to.be.undefined;
1040+
});
1041+
1042+
it('should handle empty response object', async () => {
1043+
const path = '/example-path';
1044+
1045+
nock('https://admin.hlx.page')
1046+
.get('/status/owner/repo/main/example-path?editUrl=auto')
1047+
.reply(200, {});
1048+
1049+
const result = await client.getEditURL(path);
1050+
1051+
expect(result).to.be.undefined;
1052+
});
1053+
1054+
it('should remove leading slashes from path in API call', async () => {
1055+
const path = '///multiple/leading/slashes';
1056+
const mockResponse = {
1057+
edit: { url: 'https://sharepoint.com/document/edit' },
1058+
};
1059+
1060+
const scope = nock('https://admin.hlx.page')
1061+
.get('/status/owner/repo/main/multiple/leading/slashes?editUrl=auto')
1062+
.reply(200, mockResponse);
1063+
1064+
await client.getEditURL(path);
1065+
1066+
expect(scope.isDone()).to.be.true;
1067+
});
1068+
1069+
it('should include editUrl=auto query parameter', async () => {
1070+
const path = '/test-path';
1071+
const mockResponse = {
1072+
edit: { url: 'https://onedrive.com/document/edit' },
1073+
};
1074+
1075+
const scope = nock('https://admin.hlx.page')
1076+
.get((uri) => uri.includes('editUrl=auto'))
1077+
.reply(200, mockResponse);
1078+
1079+
await client.getEditURL(path);
1080+
1081+
expect(scope.isDone()).to.be.true;
1082+
});
1083+
1084+
it('should throw an error on HTTP failure', async () => {
1085+
const path = '/example-path';
1086+
1087+
nock('https://admin.hlx.page')
1088+
.get('/status/owner/repo/main/example-path?editUrl=auto')
1089+
.reply(404, { message: 'Not Found' });
1090+
1091+
try {
1092+
await client.getEditURL(path);
1093+
expect.fail('Should have thrown an error');
1094+
} catch (err) {
1095+
expect(err.message).to.equal('Failed to fetch document path for /example-path: {"message":"Not Found"}');
1096+
}
1097+
});
1098+
1099+
it('should throw an error on network failure', async () => {
1100+
const path = '/example-path';
1101+
1102+
nock('https://admin.hlx.page')
1103+
.get('/status/owner/repo/main/example-path?editUrl=auto')
1104+
.replyWithError('Network error');
1105+
1106+
try {
1107+
await client.getEditURL(path);
1108+
expect.fail('Should have thrown an error');
1109+
} catch (err) {
1110+
expect(err.message).to.include('Network error');
1111+
}
1112+
});
1113+
1114+
it('should handle OneDrive edit URLs', async () => {
1115+
const path = '/onedrive-document';
1116+
const mockResponse = {
1117+
edit: { url: 'https://adobe.sharepoint.com/:w:/r/sites/test/_layouts/15/Doc.aspx?sourcedoc=123' },
1118+
};
1119+
1120+
nock('https://admin.hlx.page')
1121+
.get('/status/owner/repo/main/onedrive-document?editUrl=auto')
1122+
.reply(200, mockResponse);
1123+
1124+
const result = await client.getEditURL(path);
1125+
1126+
expect(result).to.equal('https://adobe.sharepoint.com/:w:/r/sites/test/_layouts/15/Doc.aspx?sourcedoc=123');
1127+
});
1128+
1129+
it('should handle Google Drive edit URLs', async () => {
1130+
const path = '/gdrive-document';
1131+
const mockResponse = {
1132+
edit: { url: 'https://docs.google.com/document/d/1abc_DEF-xyz/edit' },
1133+
};
1134+
1135+
nock('https://admin.hlx.page')
1136+
.get('/status/owner/repo/main/gdrive-document?editUrl=auto')
1137+
.reply(200, mockResponse);
1138+
1139+
const result = await client.getEditURL(path);
1140+
1141+
expect(result).to.equal('https://docs.google.com/document/d/1abc_DEF-xyz/edit');
1142+
});
1143+
1144+
it('should handle paths ending with /', async () => {
1145+
const path = '/example-path/';
1146+
const mockResponse = {
1147+
edit: { url: 'https://drive.google.com/document/d/test/edit' },
1148+
};
1149+
1150+
nock('https://admin.hlx.page')
1151+
.get('/status/owner/repo/main/example-path/?editUrl=auto')
1152+
.reply(200, mockResponse);
1153+
1154+
const result = await client.getEditURL(path);
1155+
1156+
expect(result).to.equal('https://drive.google.com/document/d/test/edit');
1157+
});
1158+
});
1159+
9701160
describe('updateImageAltText', () => {
9711161
let client;
9721162
let mockDocument;

0 commit comments

Comments
 (0)