sb-mig 6.1.0-beta.2 → 6.1.0-beta.3
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.
|
@@ -39,6 +39,7 @@ export interface LanguagePublishStateMap {
|
|
|
39
39
|
statesByLanguage?: Record<string, Record<string, number>>;
|
|
40
40
|
stories?: Record<string, LanguagePublishStateMapEntry>;
|
|
41
41
|
}
|
|
42
|
+
export declare const createStatusPreservingFetch: (baseFetch?: typeof fetch) => typeof fetch;
|
|
42
43
|
export declare const loadLanguagePublishStateMap: (languagePublishStatePath: string) => LanguagePublishStateMap;
|
|
43
44
|
export declare const buildLanguagePublishStateMap: (args: BuildLanguagePublishStateMapArgs, config: RequestBaseConfig) => Promise<LanguagePublishStateMap>;
|
|
44
45
|
export declare const buildLanguagePublishStateMapFromStories: (args: BuildLanguagePublishStateMapFromStoriesArgs, config: RequestBaseConfig) => Promise<LanguagePublishStateMap>;
|
|
@@ -1,12 +1,93 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import StoryblokClient from "storyblok-js-client";
|
|
4
5
|
import { mapWithConcurrency } from "../../utils/async-utils.js";
|
|
5
6
|
import { createAndSaveToFile } from "../../utils/files.js";
|
|
6
7
|
import Logger from "../../utils/logger.js";
|
|
7
8
|
import { getAllStories, getStoryBySlug } from "./stories.js";
|
|
8
9
|
const DEFAULT_LANGUAGE = "[default]";
|
|
9
10
|
const DELIVERY_CHECK_CONCURRENCY = 5;
|
|
11
|
+
const DELIVERY_API_DEFAULT_RATE_LIMIT = 5;
|
|
12
|
+
const DELIVERY_API_MAX_RETRIES = 10;
|
|
13
|
+
const DELIVERY_API_TIMEOUT_SECONDS = 60;
|
|
14
|
+
export const createStatusPreservingFetch = (baseFetch = fetch) => async (input, init) => {
|
|
15
|
+
const response = await baseFetch(input, init);
|
|
16
|
+
if (response.ok) {
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
const responseText = await response
|
|
20
|
+
.clone()
|
|
21
|
+
.text()
|
|
22
|
+
.catch(() => "");
|
|
23
|
+
if (responseText.trim().length === 0) {
|
|
24
|
+
return new Response("{}", {
|
|
25
|
+
status: response.status,
|
|
26
|
+
statusText: response.statusText,
|
|
27
|
+
headers: response.headers,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
JSON.parse(responseText);
|
|
32
|
+
return response;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return new Response(JSON.stringify({ error: responseText }), {
|
|
36
|
+
status: response.status,
|
|
37
|
+
statusText: response.statusText,
|
|
38
|
+
headers: response.headers,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const createDeliveryClient = ({ accessToken, deliveryApiUrl, rateLimit, }) => new StoryblokClient({
|
|
43
|
+
accessToken,
|
|
44
|
+
rateLimit: rateLimit ?? DELIVERY_API_DEFAULT_RATE_LIMIT,
|
|
45
|
+
maxRetries: DELIVERY_API_MAX_RETRIES,
|
|
46
|
+
timeout: DELIVERY_API_TIMEOUT_SECONDS,
|
|
47
|
+
fetch: createStatusPreservingFetch(),
|
|
48
|
+
cache: {
|
|
49
|
+
clear: "auto",
|
|
50
|
+
type: "none",
|
|
51
|
+
},
|
|
52
|
+
}, deliveryApiUrl);
|
|
53
|
+
const resolveStoryblokErrorStatus = (error) => error?.status ??
|
|
54
|
+
error?.response?.status ??
|
|
55
|
+
error?.response?.response?.status ??
|
|
56
|
+
error?.response?.data?.status ??
|
|
57
|
+
error?.message?.status ??
|
|
58
|
+
error?.message?.response?.status ??
|
|
59
|
+
error?.message?.response?.response?.status ??
|
|
60
|
+
error?.message?.response?.data?.status ??
|
|
61
|
+
0;
|
|
62
|
+
const resolveStoryblokErrorMessage = (error) => {
|
|
63
|
+
const responseData = error?.response?.data;
|
|
64
|
+
if (Array.isArray(responseData)) {
|
|
65
|
+
return responseData.filter(Boolean).join(", ") || null;
|
|
66
|
+
}
|
|
67
|
+
if (typeof responseData === "string") {
|
|
68
|
+
return responseData;
|
|
69
|
+
}
|
|
70
|
+
if (responseData && typeof responseData === "object") {
|
|
71
|
+
return (responseData.error ||
|
|
72
|
+
responseData.message ||
|
|
73
|
+
error?.response?.message ||
|
|
74
|
+
error?.message?.message ||
|
|
75
|
+
error?.message ||
|
|
76
|
+
null);
|
|
77
|
+
}
|
|
78
|
+
if (error?.message && typeof error.message === "object") {
|
|
79
|
+
return (error.message.message ||
|
|
80
|
+
error.message.response?.message ||
|
|
81
|
+
error.message.response?.data?.error ||
|
|
82
|
+
null);
|
|
83
|
+
}
|
|
84
|
+
return error?.message || null;
|
|
85
|
+
};
|
|
86
|
+
const isStoryblokNotFoundMessage = (message) => {
|
|
87
|
+
const normalized = message?.trim().toLowerCase();
|
|
88
|
+
return (normalized === "not found" ||
|
|
89
|
+
normalized === "this record could not be found");
|
|
90
|
+
};
|
|
10
91
|
const cleanStoryblokContent = (value) => {
|
|
11
92
|
if (Array.isArray(value)) {
|
|
12
93
|
return value.map(cleanStoryblokContent);
|
|
@@ -89,24 +170,45 @@ const classifyTranslatedLanguageState = ({ published, draft, }) => {
|
|
|
89
170
|
}
|
|
90
171
|
return "error";
|
|
91
172
|
};
|
|
92
|
-
const fetchDeliveryStory = async ({
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
173
|
+
const fetchDeliveryStory = async ({ deliveryClient, slug, version, language, }) => {
|
|
174
|
+
const params = {
|
|
175
|
+
version,
|
|
176
|
+
cv: String(Date.now()),
|
|
177
|
+
};
|
|
97
178
|
if (language && language !== DEFAULT_LANGUAGE) {
|
|
98
|
-
|
|
179
|
+
params.language = language;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const response = await deliveryClient.get(`cdn/stories/${slug}`, params);
|
|
183
|
+
const story = response?.data?.story;
|
|
184
|
+
return {
|
|
185
|
+
status: 200,
|
|
186
|
+
ok: true,
|
|
187
|
+
fullSlug: story?.full_slug || null,
|
|
188
|
+
publishedAt: story?.published_at || null,
|
|
189
|
+
contentHash: story?.content ? hashContent(story.content) : null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const errorMessage = resolveStoryblokErrorMessage(error);
|
|
194
|
+
const status = resolveStoryblokErrorStatus(error) ||
|
|
195
|
+
(isStoryblokNotFoundMessage(errorMessage) ? 404 : 0);
|
|
196
|
+
const story = error?.response?.data?.story;
|
|
197
|
+
if (status === 429) {
|
|
198
|
+
throw new Error(`Storyblok Delivery API rate limit did not recover after retries for '${slug}' (${version}${language ? `, ${language}` : ""}).`);
|
|
199
|
+
}
|
|
200
|
+
if (status === 0) {
|
|
201
|
+
throw new Error(`Storyblok Delivery API request failed without an HTTP status for '${slug}' (${version}${language ? `, ${language}` : ""})${errorMessage ? `: ${errorMessage}` : "."}`);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
status,
|
|
205
|
+
ok: false,
|
|
206
|
+
fullSlug: story?.full_slug || null,
|
|
207
|
+
publishedAt: story?.published_at || null,
|
|
208
|
+
contentHash: story?.content ? hashContent(story.content) : null,
|
|
209
|
+
errorMessage,
|
|
210
|
+
};
|
|
99
211
|
}
|
|
100
|
-
const response = await fetch(url);
|
|
101
|
-
const data = await response.json().catch(() => null);
|
|
102
|
-
const story = data?.story;
|
|
103
|
-
return {
|
|
104
|
-
status: response.status,
|
|
105
|
-
ok: response.ok,
|
|
106
|
-
fullSlug: story?.full_slug || null,
|
|
107
|
-
publishedAt: story?.published_at || null,
|
|
108
|
-
contentHash: story?.content ? hashContent(story.content) : null,
|
|
109
|
-
};
|
|
110
212
|
};
|
|
111
213
|
const loadStoriesForLanguageState = async ({ startsWith, withSlug, }, config) => {
|
|
112
214
|
if (withSlug && withSlug.length > 0) {
|
|
@@ -184,7 +286,20 @@ export const buildLanguagePublishStateMapFromStories = async (args, config) => {
|
|
|
184
286
|
const deliveryAccessToken = accessToken || "";
|
|
185
287
|
const deliveryApiUrl = config.storyblokDeliveryApiUrl || "https://api.storyblok.com/v2";
|
|
186
288
|
const uniqueLanguages = Array.from(new Set(args.languages));
|
|
289
|
+
const translatedLanguages = uniqueLanguages.filter((language) => language !== DEFAULT_LANGUAGE);
|
|
187
290
|
const stories = args.stories.filter((item) => item?.story && item.story.is_folder !== true);
|
|
291
|
+
const deliveryClient = needsDeliveryApi
|
|
292
|
+
? createDeliveryClient({
|
|
293
|
+
accessToken: deliveryAccessToken,
|
|
294
|
+
deliveryApiUrl,
|
|
295
|
+
rateLimit: config.rateLimit,
|
|
296
|
+
})
|
|
297
|
+
: null;
|
|
298
|
+
const totalDeliveryChecks = stories.length * translatedLanguages.length * 2;
|
|
299
|
+
if (needsDeliveryApi) {
|
|
300
|
+
Logger.log(`Resolving translated language publish state for ${stories.length} stories and ${translatedLanguages.length} language(s). Up to ${totalDeliveryChecks} Delivery API check(s) will run through StoryblokClient at ${config.rateLimit ?? DELIVERY_API_DEFAULT_RATE_LIMIT} req/s.`);
|
|
301
|
+
}
|
|
302
|
+
let processedStories = 0;
|
|
188
303
|
const storyEntries = await mapWithConcurrency(stories, DELIVERY_CHECK_CONCURRENCY, async (item) => {
|
|
189
304
|
const story = item.story;
|
|
190
305
|
const languagesByCode = {};
|
|
@@ -202,15 +317,13 @@ export const buildLanguagePublishStateMapFromStories = async (args, config) => {
|
|
|
202
317
|
}
|
|
203
318
|
const [published, draft] = await Promise.all([
|
|
204
319
|
fetchDeliveryStory({
|
|
205
|
-
|
|
206
|
-
accessToken: deliveryAccessToken,
|
|
320
|
+
deliveryClient: deliveryClient,
|
|
207
321
|
slug: story.full_slug,
|
|
208
322
|
version: "published",
|
|
209
323
|
language,
|
|
210
324
|
}),
|
|
211
325
|
fetchDeliveryStory({
|
|
212
|
-
|
|
213
|
-
accessToken: deliveryAccessToken,
|
|
326
|
+
deliveryClient: deliveryClient,
|
|
214
327
|
slug: story.full_slug,
|
|
215
328
|
version: "draft",
|
|
216
329
|
language,
|
|
@@ -226,7 +339,7 @@ export const buildLanguagePublishStateMapFromStories = async (args, config) => {
|
|
|
226
339
|
draft,
|
|
227
340
|
};
|
|
228
341
|
}
|
|
229
|
-
|
|
342
|
+
const storyEntry = {
|
|
230
343
|
id: story.id,
|
|
231
344
|
uuid: story.uuid,
|
|
232
345
|
name: story.name,
|
|
@@ -236,6 +349,13 @@ export const buildLanguagePublishStateMapFromStories = async (args, config) => {
|
|
|
236
349
|
.filter(([, value]) => value.state === "published_clean")
|
|
237
350
|
.map(([language]) => language),
|
|
238
351
|
};
|
|
352
|
+
processedStories++;
|
|
353
|
+
if (needsDeliveryApi &&
|
|
354
|
+
(processedStories % 10 === 0 ||
|
|
355
|
+
processedStories === stories.length)) {
|
|
356
|
+
Logger.success(`Resolved translated language publish state for ${processedStories}/${stories.length} stories.`);
|
|
357
|
+
}
|
|
358
|
+
return storyEntry;
|
|
239
359
|
});
|
|
240
360
|
return {
|
|
241
361
|
generatedAt: new Date().toISOString(),
|
|
@@ -166,10 +166,21 @@ const subtractLanguages = (allLanguages, publishLanguages) => {
|
|
|
166
166
|
return allLanguages.filter((language) => !publishSet.has(language));
|
|
167
167
|
};
|
|
168
168
|
const formatLanguageList = (languages) => languages && languages.length > 0 ? languages.join(",") : "none";
|
|
169
|
+
const resolveStoryblokErrorStatus = (err) => err?.status ??
|
|
170
|
+
err?.response?.status ??
|
|
171
|
+
err?.response?.response?.status ??
|
|
172
|
+
err?.message?.status ??
|
|
173
|
+
err?.message?.response?.status;
|
|
169
174
|
const resolveStoryblokErrorResponse = (err) => {
|
|
170
175
|
if (typeof err?.response === "string" && err.response.trim().length > 0) {
|
|
171
176
|
return err.response.trim();
|
|
172
177
|
}
|
|
178
|
+
if (Array.isArray(err?.response?.data)) {
|
|
179
|
+
const message = err.response.data.filter(Boolean).join(", ").trim();
|
|
180
|
+
if (message.length > 0) {
|
|
181
|
+
return message;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
173
184
|
if (typeof err?.response?.data === "string" &&
|
|
174
185
|
err.response.data.trim().length > 0) {
|
|
175
186
|
return err.response.data.trim();
|
|
@@ -178,6 +189,23 @@ const resolveStoryblokErrorResponse = (err) => {
|
|
|
178
189
|
err.response.message.trim().length > 0) {
|
|
179
190
|
return err.response.message.trim();
|
|
180
191
|
}
|
|
192
|
+
if (typeof err?.message?.message === "string" &&
|
|
193
|
+
err.message.message.trim().length > 0) {
|
|
194
|
+
return err.message.message.trim();
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(err?.message?.response?.data)) {
|
|
197
|
+
const message = err.message.response.data
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.join(", ")
|
|
200
|
+
.trim();
|
|
201
|
+
if (message.length > 0) {
|
|
202
|
+
return message;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (typeof err?.message?.response?.data === "string" &&
|
|
206
|
+
err.message.response.data.trim().length > 0) {
|
|
207
|
+
return err.message.response.data.trim();
|
|
208
|
+
}
|
|
181
209
|
if (typeof err?.message === "string" && err.message.trim().length > 0) {
|
|
182
210
|
return err.message.trim();
|
|
183
211
|
}
|
|
@@ -257,7 +285,16 @@ export const getStoryById = (storyId, config) => {
|
|
|
257
285
|
}
|
|
258
286
|
return res.data;
|
|
259
287
|
})
|
|
260
|
-
.catch((err) =>
|
|
288
|
+
.catch((err) => {
|
|
289
|
+
const status = resolveStoryblokErrorStatus(err);
|
|
290
|
+
const responseMessage = resolveStoryblokErrorResponse(err);
|
|
291
|
+
const statusLabel = status ? `status ${status}` : "unknown status";
|
|
292
|
+
const responseLabel = responseMessage
|
|
293
|
+
? ` Response: ${responseMessage}`
|
|
294
|
+
: "";
|
|
295
|
+
Logger.error(`Failed to fetch story '${storyId}' with full content from space '${spaceId}' (${statusLabel}).${responseLabel}`);
|
|
296
|
+
return undefined;
|
|
297
|
+
});
|
|
261
298
|
};
|
|
262
299
|
export const getStoryBySlug = async (slug, config) => {
|
|
263
300
|
const { spaceId, sbApi } = config;
|
|
@@ -175,10 +175,21 @@ const subtractLanguages = (allLanguages, publishLanguages) => {
|
|
|
175
175
|
return allLanguages.filter((language) => !publishSet.has(language));
|
|
176
176
|
};
|
|
177
177
|
const formatLanguageList = (languages) => languages && languages.length > 0 ? languages.join(",") : "none";
|
|
178
|
+
const resolveStoryblokErrorStatus = (err) => err?.status ??
|
|
179
|
+
err?.response?.status ??
|
|
180
|
+
err?.response?.response?.status ??
|
|
181
|
+
err?.message?.status ??
|
|
182
|
+
err?.message?.response?.status;
|
|
178
183
|
const resolveStoryblokErrorResponse = (err) => {
|
|
179
184
|
if (typeof err?.response === "string" && err.response.trim().length > 0) {
|
|
180
185
|
return err.response.trim();
|
|
181
186
|
}
|
|
187
|
+
if (Array.isArray(err?.response?.data)) {
|
|
188
|
+
const message = err.response.data.filter(Boolean).join(", ").trim();
|
|
189
|
+
if (message.length > 0) {
|
|
190
|
+
return message;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
182
193
|
if (typeof err?.response?.data === "string" &&
|
|
183
194
|
err.response.data.trim().length > 0) {
|
|
184
195
|
return err.response.data.trim();
|
|
@@ -187,6 +198,23 @@ const resolveStoryblokErrorResponse = (err) => {
|
|
|
187
198
|
err.response.message.trim().length > 0) {
|
|
188
199
|
return err.response.message.trim();
|
|
189
200
|
}
|
|
201
|
+
if (typeof err?.message?.message === "string" &&
|
|
202
|
+
err.message.message.trim().length > 0) {
|
|
203
|
+
return err.message.message.trim();
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(err?.message?.response?.data)) {
|
|
206
|
+
const message = err.message.response.data
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.join(", ")
|
|
209
|
+
.trim();
|
|
210
|
+
if (message.length > 0) {
|
|
211
|
+
return message;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (typeof err?.message?.response?.data === "string" &&
|
|
215
|
+
err.message.response.data.trim().length > 0) {
|
|
216
|
+
return err.message.response.data.trim();
|
|
217
|
+
}
|
|
190
218
|
if (typeof err?.message === "string" && err.message.trim().length > 0) {
|
|
191
219
|
return err.message.trim();
|
|
192
220
|
}
|
|
@@ -269,7 +297,16 @@ const getStoryById = (storyId, config) => {
|
|
|
269
297
|
}
|
|
270
298
|
return res.data;
|
|
271
299
|
})
|
|
272
|
-
.catch((err) =>
|
|
300
|
+
.catch((err) => {
|
|
301
|
+
const status = resolveStoryblokErrorStatus(err);
|
|
302
|
+
const responseMessage = resolveStoryblokErrorResponse(err);
|
|
303
|
+
const statusLabel = status ? `status ${status}` : "unknown status";
|
|
304
|
+
const responseLabel = responseMessage
|
|
305
|
+
? ` Response: ${responseMessage}`
|
|
306
|
+
: "";
|
|
307
|
+
logger_js_1.default.error(`Failed to fetch story '${storyId}' with full content from space '${spaceId}' (${statusLabel}).${responseLabel}`);
|
|
308
|
+
return undefined;
|
|
309
|
+
});
|
|
273
310
|
};
|
|
274
311
|
exports.getStoryById = getStoryById;
|
|
275
312
|
const getStoryBySlug = async (slug, config) => {
|
package/package.json
CHANGED