@adobe/spacecat-shared-content-client 1.0.7 → 1.1.0
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 +14 -0
- package/package.json +4 -3
- package/src/clients/content-client.js +129 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@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)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* update redirects ([719b29d](https://github.com/adobe/spacecat-shared/commit/719b29dc4b8d267e68f263abd29eafd79a925365))
|
|
7
|
+
|
|
8
|
+
# [@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)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* keep image in get and update page metadata table ([251a96c](https://github.com/adobe/spacecat-shared/commit/251a96cd61a55ab9e305babf68a3f5c3db273e39))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-content-client-v1.0.7](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-content-client-v1.0.6...@adobe/spacecat-shared-content-client-v1.0.7) (2024-09-14)
|
|
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
|
|
3
|
+
"version": "1.1.0",
|
|
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.
|
|
39
|
-
"@adobe/spacecat-shared-utils": "1.19.6"
|
|
38
|
+
"@adobe/spacecat-helix-content-sdk": "1.1.10",
|
|
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';
|
|
@@ -91,12 +92,90 @@ const validateMetadata = (metadata) => {
|
|
|
91
92
|
throw new Error(`Metadata key ${key} must be a string`);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
if (!hasText(value)) {
|
|
95
|
-
throw new Error(`Metadata value for key ${key} must be a
|
|
95
|
+
if (!hasText(value.value) || !hasText(value.type)) {
|
|
96
|
+
throw new Error(`Metadata value for key ${key} must be a object that has a value and type`);
|
|
96
97
|
}
|
|
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,15 +246,16 @@ 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);
|
|
177
|
-
const originalMetadata = await this.getPageMetadata(
|
|
258
|
+
const originalMetadata = await this.getPageMetadata(path);
|
|
178
259
|
|
|
179
260
|
let mergedMetadata;
|
|
180
261
|
if (overwrite) {
|
|
@@ -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
|
}
|