@adobe/spacecat-shared-content-client 1.0.8 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [@adobe/spacecat-shared-content-client-v1.1.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-content-client-v1.1.0...@adobe/spacecat-shared-content-client-v1.1.1) (2024-09-21)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deps:** update adobe fixes ([#374](https://github.com/adobe/spacecat-shared/issues/374)) ([426e61b](https://github.com/adobe/spacecat-shared/commit/426e61b2e77a955a33651245344724881b0f4f55))
7
+
8
+ # [@adobe/spacecat-shared-content-client-v1.1.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-content-client-v1.0.8...@adobe/spacecat-shared-content-client-v1.1.0) (2024-09-19)
9
+
10
+
11
+ ### Features
12
+
13
+ * update redirects ([719b29d](https://github.com/adobe/spacecat-shared/commit/719b29dc4b8d267e68f263abd29eafd79a925365))
14
+
1
15
  # [@adobe/spacecat-shared-content-client-v1.0.8](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-content-client-v1.0.7...@adobe/spacecat-shared-content-client-v1.0.8) (2024-09-19)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-content-client",
3
- "version": "1.0.8",
3
+ "version": "1.1.1",
4
4
  "description": "Shared modules of the Spacecat Services - Content Client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,8 +35,9 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@adobe/helix-universal": "5.0.5",
38
- "@adobe/spacecat-helix-content-sdk": "1.1.10",
39
- "@adobe/spacecat-shared-utils": "1.19.6"
38
+ "@adobe/spacecat-helix-content-sdk": "1.1.11",
39
+ "@adobe/spacecat-shared-utils": "1.19.6",
40
+ "graph-data-structure": "4.0.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "chai": "5.1.1",
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { createFrom as createContentSDKClient } from '@adobe/spacecat-helix-content-sdk';
14
14
  import { hasText, isObject } from '@adobe/spacecat-shared-utils';
15
+ import { Graph, hasCycle } from 'graph-data-structure';
15
16
 
16
17
  const CONTENT_SOURCE_TYPE_DRIVE_GOOGLE = 'drive.google';
17
18
  const CONTENT_SOURCE_TYPE_ONEDRIVE = 'onedrive';
@@ -97,6 +98,84 @@ const validateMetadata = (metadata) => {
97
98
  }
98
99
  };
99
100
 
101
+ const validateRedirects = (redirects) => {
102
+ const pathRegex = /^\/[a-zA-Z0-9\-._~%!$&'()*+,;=:@/]*$/;
103
+ if (!Array.isArray(redirects)) {
104
+ throw new Error('Redirects must be an array');
105
+ }
106
+
107
+ if (!redirects.length) {
108
+ throw new Error('Redirects must not be empty');
109
+ }
110
+
111
+ for (const redirect of redirects) {
112
+ if (!isObject(redirect)) {
113
+ throw new Error('Redirect must be an object');
114
+ }
115
+
116
+ if (!hasText(redirect.from)) {
117
+ throw new Error('Redirect must have a valid from path');
118
+ }
119
+
120
+ if (!hasText(redirect.to)) {
121
+ throw new Error('Redirect must have a valid to path');
122
+ }
123
+
124
+ if (!pathRegex.test(redirect.from)) {
125
+ throw new Error(`Invalid redirect from path: ${redirect.from}`);
126
+ }
127
+
128
+ if (!pathRegex.test(redirect.to)) {
129
+ throw new Error(`Invalid redirect to path: ${redirect.to}`);
130
+ }
131
+
132
+ if (redirect.from === redirect.to) {
133
+ throw new Error('Redirect from and to paths must be different');
134
+ }
135
+ }
136
+ };
137
+
138
+ const removeDuplicatedRedirects = (currentRedirects, newRedirects, log) => {
139
+ const redirectsSet = new Set(
140
+ currentRedirects.map(({ from, to }) => `${from}:${to}`),
141
+ );
142
+
143
+ const newRedirectsClean = [];
144
+ newRedirects.forEach((redirectRule) => {
145
+ const { from, to } = redirectRule;
146
+ const strRedirectRule = `${from}:${to}`;
147
+ if (!redirectsSet.has(strRedirectRule)) {
148
+ redirectsSet.add(strRedirectRule);
149
+ newRedirectsClean.push(redirectRule);
150
+ } else {
151
+ log.info(`Duplicate redirect rule detected: ${strRedirectRule}`);
152
+ }
153
+ });
154
+ return newRedirectsClean;
155
+ };
156
+
157
+ const removeRedirectLoops = (currentRedirects, newRedirects, log) => {
158
+ const redirectsGraph = new Graph();
159
+ const noCycleRedirects = [];
160
+ currentRedirects.forEach((r) => redirectsGraph.addEdge(r.from, r.to));
161
+ if (hasCycle(redirectsGraph)) {
162
+ throw new Error('Redirect cycle detected in current redirects');
163
+ }
164
+ newRedirects.forEach((r) => {
165
+ redirectsGraph.addEdge(r.from, r.to);
166
+ if (hasCycle(redirectsGraph)) {
167
+ log.info(`Redirect loop detected: ${r.from} -> ${r.to}`);
168
+ redirectsGraph.removeEdge(r.from, r.to);
169
+ } else {
170
+ noCycleRedirects.push(r);
171
+ }
172
+ });
173
+ if (newRedirects.length !== noCycleRedirects.length) {
174
+ log.info(`Removed ${newRedirects.length - noCycleRedirects.length} redirect loops`);
175
+ }
176
+ return noCycleRedirects;
177
+ };
178
+
100
179
  export default class ContentClient {
101
180
  static createFrom(context, site) {
102
181
  const { log = console, env } = context;
@@ -149,10 +228,11 @@ export default class ContentClient {
149
228
 
150
229
  async getPageMetadata(path) {
151
230
  const startTime = process.hrtime.bigint();
152
- await this.#initClient();
153
231
 
154
232
  validatePath(path);
155
233
 
234
+ await this.#initClient();
235
+
156
236
  this.log.info(`Getting page metadata for ${this.site.getId()} and path ${path}`);
157
237
 
158
238
  const docPath = this.#resolveDocPath(path);
@@ -166,11 +246,12 @@ export default class ContentClient {
166
246
  async updatePageMetadata(path, metadata, options = {}) {
167
247
  const { overwrite = true } = options;
168
248
  const startTime = process.hrtime.bigint();
169
- await this.#initClient();
170
249
 
171
250
  validatePath(path);
172
251
  validateMetadata(metadata);
173
252
 
253
+ await this.#initClient();
254
+
174
255
  this.log.info(`Updating page metadata for ${this.site.getId()} and path ${path}`);
175
256
 
176
257
  const docPath = this.#resolveDocPath(path);
@@ -192,4 +273,47 @@ export default class ContentClient {
192
273
 
193
274
  return mergedMetadata;
194
275
  }
276
+
277
+ async getRedirects() {
278
+ const startTime = process.hrtime.bigint();
279
+ await this.#initClient();
280
+
281
+ this.log.info(`Getting redirects for ${this.site.getId()}`);
282
+
283
+ const redirects = await this.rawClient.getRedirects();
284
+ this.#logDuration('getRedirects', startTime);
285
+
286
+ return redirects;
287
+ }
288
+
289
+ async updateRedirects(redirects) {
290
+ const startTime = process.hrtime.bigint();
291
+
292
+ validateRedirects(redirects);
293
+
294
+ await this.#initClient();
295
+
296
+ this.log.info(`Updating redirects for ${this.site.getId()}`);
297
+
298
+ const currentRedirects = await this.getRedirects();
299
+
300
+ // validate combination of existing and new redirects
301
+ const cleanNewRedirects = removeDuplicatedRedirects(currentRedirects, redirects, this.log);
302
+ if (cleanNewRedirects.length === 0) {
303
+ this.log.info('No valid redirects to update');
304
+ return;
305
+ }
306
+ const noCycleRedirects = removeRedirectLoops(currentRedirects, cleanNewRedirects, this.log);
307
+ if (noCycleRedirects.length === 0) {
308
+ this.log.info('No valid redirects to update');
309
+ return;
310
+ }
311
+
312
+ const response = await this.rawClient.appendRedirects(noCycleRedirects);
313
+ if (response.status !== 200) {
314
+ throw new Error('Failed to update redirects');
315
+ }
316
+
317
+ this.#logDuration('updateRedirects', startTime);
318
+ }
195
319
  }