storyblok 4.12.1 → 4.14.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/dist/index.mjs +2643 -192
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import 'dotenv/config';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import { resolve, dirname, isAbsolute, relative as relative$1, join as join$1 } from 'pathe';
|
|
5
5
|
import { existsSync, mkdirSync, appendFileSync, writeFileSync, readdirSync, unlinkSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
@@ -8,10 +8,10 @@ import { loadConfig as loadConfig$1, SUPPORTED_EXTENSIONS } from 'c12';
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { readPackageUp } from 'read-package-up';
|
|
10
10
|
import { Command } from 'commander';
|
|
11
|
-
import path, { join, resolve as resolve$1, parse, dirname as dirname$1, extname, relative } from 'node:path';
|
|
11
|
+
import path, { join, resolve as resolve$1, parse, dirname as dirname$1, extname, relative, basename } from 'node:path';
|
|
12
12
|
import { MultiBar, Presets } from 'cli-progress';
|
|
13
13
|
import { Spinner } from '@topcli/spinner';
|
|
14
|
-
import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, readdir } from 'node:fs/promises';
|
|
14
|
+
import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, constants, readdir, unlink, stat } from 'node:fs/promises';
|
|
15
15
|
import filenamify from 'filenamify';
|
|
16
16
|
import { select, password, input, confirm } from '@inquirer/prompts';
|
|
17
17
|
import { ManagementApiClient } from '@storyblok/management-api-client';
|
|
@@ -24,6 +24,10 @@ import { hash } from 'ohash';
|
|
|
24
24
|
import { compile } from 'json-schema-to-typescript';
|
|
25
25
|
import open from 'open';
|
|
26
26
|
import { Octokit } from 'octokit';
|
|
27
|
+
import { pipeline as pipeline$1 } from 'node:stream/promises';
|
|
28
|
+
import { Buffer } from 'node:buffer';
|
|
29
|
+
import Storyblok from 'storyblok-js-client';
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
27
31
|
|
|
28
32
|
const commands = {
|
|
29
33
|
LOGIN: "login",
|
|
@@ -37,7 +41,9 @@ const commands = {
|
|
|
37
41
|
DATASOURCES: "datasources",
|
|
38
42
|
CREATE: "create",
|
|
39
43
|
LOGS: "logs",
|
|
40
|
-
REPORTS: "reports"
|
|
44
|
+
REPORTS: "reports",
|
|
45
|
+
ASSETS: "assets",
|
|
46
|
+
STORIES: "stories"
|
|
41
47
|
};
|
|
42
48
|
const colorPalette = {
|
|
43
49
|
PRIMARY: "#8d60ff",
|
|
@@ -55,7 +61,9 @@ const colorPalette = {
|
|
|
55
61
|
PRESETS: "#a855f7",
|
|
56
62
|
DATASOURCES: "#4ade80",
|
|
57
63
|
LOGS: "#4ade80",
|
|
58
|
-
REPORTS: "#4ade80"
|
|
64
|
+
REPORTS: "#4ade80",
|
|
65
|
+
ASSETS: "#f97316",
|
|
66
|
+
STORIES: "#a185ff"
|
|
59
67
|
};
|
|
60
68
|
const regions = {
|
|
61
69
|
EU: "eu",
|
|
@@ -96,10 +104,12 @@ const regionNames = {
|
|
|
96
104
|
SB_Agent_Version: process.env.npm_package_version || "4.x"
|
|
97
105
|
});
|
|
98
106
|
const directories = {
|
|
99
|
-
|
|
100
|
-
report: "reports",
|
|
107
|
+
assets: "assets",
|
|
101
108
|
components: "components",
|
|
102
|
-
datasources: "datasources"
|
|
109
|
+
datasources: "datasources",
|
|
110
|
+
logs: "logs",
|
|
111
|
+
reports: "reports",
|
|
112
|
+
stories: "stories"
|
|
103
113
|
};
|
|
104
114
|
|
|
105
115
|
function isPlainObject(value) {
|
|
@@ -600,7 +610,17 @@ const API_ACTIONS = {
|
|
|
600
610
|
update_component_preset: "Failed to update component preset",
|
|
601
611
|
pull_stories: "Failed to pull stories",
|
|
602
612
|
pull_story: "Failed to pull story",
|
|
613
|
+
create_story: "Failed to create story",
|
|
603
614
|
update_story: "Failed to update story",
|
|
615
|
+
pull_asset: "Failed to pull asset",
|
|
616
|
+
pull_assets: "Failed to pull assets",
|
|
617
|
+
pull_asset_folder: "Failed to pull asset folder",
|
|
618
|
+
pull_asset_folders: "Failed to pull asset folders",
|
|
619
|
+
push_asset_folder: "Failed to push asset folder",
|
|
620
|
+
push_asset_sign: "Failed to sign asset upload",
|
|
621
|
+
push_asset_upload: "Failed to upload asset",
|
|
622
|
+
push_asset_finish: "Failed to finish asset upload",
|
|
623
|
+
push_asset_update: "Failed to update asset",
|
|
604
624
|
pull_datasources: "Failed to pull datasources",
|
|
605
625
|
push_datasource: "Failed to push datasource",
|
|
606
626
|
update_datasource: "Failed to update datasource",
|
|
@@ -860,7 +880,7 @@ function handleVerboseError(error) {
|
|
|
860
880
|
konsola.error(`Unexpected Error`, error);
|
|
861
881
|
}
|
|
862
882
|
}
|
|
863
|
-
function handleError(error, verbose = false) {
|
|
883
|
+
function handleError(error, verbose = false, context) {
|
|
864
884
|
if (error instanceof APIError || error instanceof FileSystemError) {
|
|
865
885
|
const messageStack = error.messageStack;
|
|
866
886
|
messageStack.forEach((message, index) => {
|
|
@@ -883,7 +903,10 @@ function handleError(error, verbose = false) {
|
|
|
883
903
|
if (!process.env.VITEST) {
|
|
884
904
|
console.log("");
|
|
885
905
|
}
|
|
886
|
-
getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR" });
|
|
906
|
+
getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR", context });
|
|
907
|
+
}
|
|
908
|
+
function logOnlyError(error, context) {
|
|
909
|
+
getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR", context });
|
|
887
910
|
}
|
|
888
911
|
|
|
889
912
|
function requireAuthentication(state, verbose = false) {
|
|
@@ -1148,19 +1171,22 @@ const saveToFileSync = (filePath, data, options) => {
|
|
|
1148
1171
|
}
|
|
1149
1172
|
};
|
|
1150
1173
|
const appendToFile = async (filePath, data, options) => {
|
|
1151
|
-
const
|
|
1152
|
-
try {
|
|
1153
|
-
await mkdir(resolvedPath, { recursive: true });
|
|
1154
|
-
} catch (mkdirError) {
|
|
1155
|
-
handleFileSystemError("mkdir", mkdirError);
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
try {
|
|
1159
|
-
const dataWithNewline = data.endsWith("\n") ? data : `${data}
|
|
1174
|
+
const dataWithNewline = data.endsWith("\n") ? data : `${data}
|
|
1160
1175
|
`;
|
|
1176
|
+
try {
|
|
1161
1177
|
await appendFile(filePath, dataWithNewline, options);
|
|
1162
|
-
} catch (
|
|
1163
|
-
|
|
1178
|
+
} catch (maybeError) {
|
|
1179
|
+
const error = toError(maybeError);
|
|
1180
|
+
if ("code" in error && error.code === "ENOENT") {
|
|
1181
|
+
const dir = parse(filePath).dir;
|
|
1182
|
+
await mkdir(dir, { recursive: true });
|
|
1183
|
+
await appendFile(filePath, dataWithNewline, options);
|
|
1184
|
+
} else {
|
|
1185
|
+
handleFileSystemError(
|
|
1186
|
+
"syscall" in error && error.syscall === "mkdir" ? "mkdir" : "write",
|
|
1187
|
+
error
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1164
1190
|
}
|
|
1165
1191
|
};
|
|
1166
1192
|
const appendToFileSync = (filePath, data, options) => {
|
|
@@ -1187,6 +1213,30 @@ const readFile = async (filePath) => {
|
|
|
1187
1213
|
return "";
|
|
1188
1214
|
}
|
|
1189
1215
|
};
|
|
1216
|
+
const loadManifest = async (manifestFile) => {
|
|
1217
|
+
return readFile$1(manifestFile, "utf8").then((manifest) => manifest.split("\n").filter(Boolean).map((entry) => JSON.parse(entry))).catch((error) => {
|
|
1218
|
+
if (error && error.code === "ENOENT") {
|
|
1219
|
+
return [];
|
|
1220
|
+
}
|
|
1221
|
+
throw error;
|
|
1222
|
+
});
|
|
1223
|
+
};
|
|
1224
|
+
const saveManifest = async (manifestFile, entries) => {
|
|
1225
|
+
const content = entries.map((entry) => JSON.stringify(entry)).join("\n");
|
|
1226
|
+
await saveToFile(manifestFile, content ? `${content}
|
|
1227
|
+
` : "");
|
|
1228
|
+
};
|
|
1229
|
+
const deduplicateManifest = async (manifestFile) => {
|
|
1230
|
+
const entries = await loadManifest(manifestFile);
|
|
1231
|
+
if (entries.length === 0) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const uniqueEntries = /* @__PURE__ */ new Map();
|
|
1235
|
+
for (const entry of entries) {
|
|
1236
|
+
uniqueEntries.set(entry.old_id, entry);
|
|
1237
|
+
}
|
|
1238
|
+
await saveManifest(manifestFile, Array.from(uniqueEntries.values()));
|
|
1239
|
+
};
|
|
1190
1240
|
const resolvePath = (path, folder) => {
|
|
1191
1241
|
const basePath = path ?? DEFAULT_STORAGE_DIR;
|
|
1192
1242
|
return resolve$1(process.cwd(), basePath, folder);
|
|
@@ -1205,6 +1255,15 @@ const sanitizeFilename = (filename) => {
|
|
|
1205
1255
|
replacement: "_"
|
|
1206
1256
|
});
|
|
1207
1257
|
};
|
|
1258
|
+
async function readDirectory(directoryPath) {
|
|
1259
|
+
try {
|
|
1260
|
+
const files = await readdir(directoryPath);
|
|
1261
|
+
return files;
|
|
1262
|
+
} catch (maybeError) {
|
|
1263
|
+
handleFileSystemError("read", toError(maybeError));
|
|
1264
|
+
return [];
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1208
1267
|
async function readJsonFile(filePath) {
|
|
1209
1268
|
try {
|
|
1210
1269
|
const content = (await readFile(filePath)).toString();
|
|
@@ -1218,7 +1277,15 @@ async function readJsonFile(filePath) {
|
|
|
1218
1277
|
}
|
|
1219
1278
|
}
|
|
1220
1279
|
function importModule(filePath) {
|
|
1221
|
-
return import(
|
|
1280
|
+
return import(pathToFileURL(filePath).href);
|
|
1281
|
+
}
|
|
1282
|
+
async function fileExists(path) {
|
|
1283
|
+
try {
|
|
1284
|
+
await access(path, constants.F_OK);
|
|
1285
|
+
return true;
|
|
1286
|
+
} catch {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1222
1289
|
}
|
|
1223
1290
|
|
|
1224
1291
|
const REPORT_STATUS = {
|
|
@@ -1399,7 +1466,7 @@ class FileTransport {
|
|
|
1399
1466
|
name: value.name,
|
|
1400
1467
|
message: value.message,
|
|
1401
1468
|
httpCode: value.code,
|
|
1402
|
-
httpStatusText: value.error?.response
|
|
1469
|
+
httpStatusText: value.error?.response?.statusText,
|
|
1403
1470
|
stack: value.stack
|
|
1404
1471
|
};
|
|
1405
1472
|
continue;
|
|
@@ -1491,7 +1558,7 @@ class ConsoleTransport {
|
|
|
1491
1558
|
name: value.name,
|
|
1492
1559
|
message: value.message,
|
|
1493
1560
|
httpCode: value.code,
|
|
1494
|
-
httpStatusText: value.error?.response
|
|
1561
|
+
httpStatusText: value.error?.response?.statusText,
|
|
1495
1562
|
stack: value.stack
|
|
1496
1563
|
});
|
|
1497
1564
|
}
|
|
@@ -1558,7 +1625,7 @@ function getProgram() {
|
|
|
1558
1625
|
}
|
|
1559
1626
|
if (resolvedConfig.log.file.enabled) {
|
|
1560
1627
|
const logsPath = resolveCommandPath(
|
|
1561
|
-
directories.
|
|
1628
|
+
directories.logs,
|
|
1562
1629
|
options.space,
|
|
1563
1630
|
options.path
|
|
1564
1631
|
);
|
|
@@ -1582,7 +1649,7 @@ function getProgram() {
|
|
|
1582
1649
|
getUI({ enabled: resolvedConfig.ui.enabled });
|
|
1583
1650
|
if (resolvedConfig.report.enabled) {
|
|
1584
1651
|
const reportPath = resolveCommandPath(
|
|
1585
|
-
directories.
|
|
1652
|
+
directories.reports,
|
|
1586
1653
|
options.space,
|
|
1587
1654
|
options.path
|
|
1588
1655
|
);
|
|
@@ -1629,21 +1696,25 @@ function configsAreEqual(config1, config2) {
|
|
|
1629
1696
|
function mapiClient(options) {
|
|
1630
1697
|
if (!instance && options) {
|
|
1631
1698
|
instance = new ManagementApiClient(options);
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1699
|
+
if (getActiveConfig().api.maxConcurrency > 0) {
|
|
1700
|
+
instance.interceptors.request.use(async (request) => {
|
|
1701
|
+
const limit = resolveLimiter();
|
|
1702
|
+
await limit();
|
|
1703
|
+
return request;
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1637
1706
|
storedConfig = options;
|
|
1638
1707
|
} else if (!instance) {
|
|
1639
1708
|
throw new Error("MAPI client not initialized. Call mapiClient with configuration first.");
|
|
1640
1709
|
} else if (options && storedConfig && !configsAreEqual(options, storedConfig)) {
|
|
1641
1710
|
instance = new ManagementApiClient(options);
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1711
|
+
if (getActiveConfig().api.maxConcurrency > 0) {
|
|
1712
|
+
instance.interceptors.request.use(async (request) => {
|
|
1713
|
+
const limit = resolveLimiter();
|
|
1714
|
+
await limit();
|
|
1715
|
+
return request;
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1647
1718
|
storedConfig = options;
|
|
1648
1719
|
}
|
|
1649
1720
|
return instance;
|
|
@@ -1862,29 +1933,123 @@ function session() {
|
|
|
1862
1933
|
return sessionInstance;
|
|
1863
1934
|
}
|
|
1864
1935
|
|
|
1865
|
-
|
|
1866
|
-
const
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1936
|
+
async function performInteractiveLogin(options) {
|
|
1937
|
+
const { verbose = false, preSelectedRegion, showWelcomeMessage = true } = options || {};
|
|
1938
|
+
const spinner = new Spinner({
|
|
1939
|
+
verbose: !isVitest
|
|
1940
|
+
});
|
|
1941
|
+
try {
|
|
1942
|
+
const strategy = await select({
|
|
1943
|
+
message: "How would you like to login?",
|
|
1944
|
+
choices: [
|
|
1945
|
+
{
|
|
1946
|
+
name: "With email",
|
|
1947
|
+
value: "login-with-email",
|
|
1948
|
+
short: "Email"
|
|
1949
|
+
},
|
|
1950
|
+
{
|
|
1951
|
+
name: "With Token (Personal Access Token \u2013 works also for SSO accounts)",
|
|
1952
|
+
value: "login-with-token",
|
|
1953
|
+
short: "Token"
|
|
1954
|
+
}
|
|
1955
|
+
]
|
|
1956
|
+
});
|
|
1957
|
+
let userToken;
|
|
1958
|
+
let userRegion;
|
|
1959
|
+
if (strategy === "login-with-token") {
|
|
1960
|
+
konsola.info([
|
|
1961
|
+
"\u{1F511} You can use a Personal Access Token to log in.",
|
|
1962
|
+
"This works for all accounts, including SSO accounts.",
|
|
1963
|
+
`Generate one in your Storyblok account settings: ${chalk.underline.blue("https://app.storyblok.com/#/me/account?tab=token")}`
|
|
1964
|
+
].join("\n"));
|
|
1965
|
+
userToken = await password({
|
|
1966
|
+
message: "Please enter your Personal Access Token:",
|
|
1967
|
+
validate: (value) => {
|
|
1968
|
+
return value.length > 0;
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
userRegion = preSelectedRegion || await select({
|
|
1972
|
+
message: "Please select the region you would like to work in:",
|
|
1973
|
+
choices: Object.values(regions).map((region) => ({
|
|
1974
|
+
name: regionNames[region],
|
|
1975
|
+
value: region
|
|
1976
|
+
})),
|
|
1977
|
+
default: regions.EU
|
|
1978
|
+
});
|
|
1979
|
+
spinner.start(`Logging in with token`);
|
|
1980
|
+
const user = await loginWithToken(userToken, userRegion);
|
|
1981
|
+
spinner.succeed();
|
|
1982
|
+
if (user) {
|
|
1983
|
+
const { updateSession, persistCredentials } = session();
|
|
1984
|
+
updateSession(user.email, userToken, userRegion);
|
|
1985
|
+
await persistCredentials(userRegion);
|
|
1986
|
+
if (showWelcomeMessage) {
|
|
1987
|
+
konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(user.friendly_name)}.`, true);
|
|
1988
|
+
}
|
|
1989
|
+
return { token: userToken, region: userRegion };
|
|
1990
|
+
}
|
|
1991
|
+
} else {
|
|
1992
|
+
const userEmail = await input({
|
|
1993
|
+
message: "Please enter your email address:",
|
|
1994
|
+
required: true,
|
|
1995
|
+
validate: (value) => {
|
|
1996
|
+
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
|
1997
|
+
return emailRegex.test(value);
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
const userPassword = await password({
|
|
2001
|
+
message: "Please enter your password:"
|
|
2002
|
+
});
|
|
2003
|
+
userRegion = preSelectedRegion || await select({
|
|
2004
|
+
message: "Please select the region you would like to work in:",
|
|
2005
|
+
choices: Object.values(regions).map((region) => ({
|
|
2006
|
+
name: regionNames[region],
|
|
2007
|
+
value: region
|
|
2008
|
+
})),
|
|
2009
|
+
default: regions.EU
|
|
2010
|
+
});
|
|
2011
|
+
spinner.start(`Logging in with email`);
|
|
2012
|
+
spinner.succeed();
|
|
2013
|
+
const response = await loginWithEmailAndPassword(userEmail, userPassword, userRegion);
|
|
2014
|
+
if (response?.otp_required) {
|
|
2015
|
+
const otp = await input({
|
|
2016
|
+
message: "Add the code from your Authenticator app, or the one we sent to your e-mail / phone:",
|
|
2017
|
+
required: true
|
|
2018
|
+
});
|
|
2019
|
+
const otpResponse = await loginWithOtp(userEmail, userPassword, otp, userRegion);
|
|
2020
|
+
if (otpResponse?.access_token) {
|
|
2021
|
+
userToken = otpResponse.access_token;
|
|
2022
|
+
}
|
|
2023
|
+
} else if (response?.access_token) {
|
|
2024
|
+
userToken = response.access_token;
|
|
2025
|
+
}
|
|
2026
|
+
if (userToken) {
|
|
2027
|
+
const { updateSession, persistCredentials } = session();
|
|
2028
|
+
updateSession(userEmail, userToken, userRegion);
|
|
2029
|
+
await persistCredentials(userRegion);
|
|
2030
|
+
if (showWelcomeMessage) {
|
|
2031
|
+
konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(userEmail)}.`, true);
|
|
2032
|
+
}
|
|
2033
|
+
return { token: userToken, region: userRegion };
|
|
2034
|
+
}
|
|
1879
2035
|
}
|
|
1880
|
-
|
|
1881
|
-
}
|
|
1882
|
-
|
|
2036
|
+
return null;
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
spinner.failed();
|
|
2039
|
+
konsola.br();
|
|
2040
|
+
handleError(error, verbose);
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const program$j = getProgram();
|
|
2046
|
+
const allRegionsText = Object.values(regions).join(",");
|
|
2047
|
+
program$j.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
|
|
1883
2048
|
"-r, --region <region>",
|
|
1884
2049
|
`The region you would like to work in. Please keep in mind that the region must match the region of your space. This region flag will be used for the other cli's commands. You can use the values: ${allRegionsText}.`
|
|
1885
2050
|
).action(async (options) => {
|
|
1886
2051
|
konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
|
|
1887
|
-
const verbose = program$
|
|
2052
|
+
const verbose = program$j.opts().verbose;
|
|
1888
2053
|
const { token, region } = options;
|
|
1889
2054
|
const { state, updateSession, persistCredentials, initializeSession } = session();
|
|
1890
2055
|
await initializeSession();
|
|
@@ -1926,85 +2091,16 @@ program$h.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
1926
2091
|
handleError(error, verbose);
|
|
1927
2092
|
}
|
|
1928
2093
|
} else {
|
|
1929
|
-
const spinner = new Spinner({
|
|
1930
|
-
verbose: !isVitest
|
|
1931
|
-
});
|
|
1932
2094
|
try {
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
const userToken = await password({
|
|
1941
|
-
message: "Please enter your Personal Access Token:",
|
|
1942
|
-
validate: (value) => {
|
|
1943
|
-
return value.length > 0;
|
|
1944
|
-
}
|
|
1945
|
-
});
|
|
1946
|
-
let userRegion = region;
|
|
1947
|
-
if (!userRegion) {
|
|
1948
|
-
userRegion = await select({
|
|
1949
|
-
message: "Please select the region you would like to work in:",
|
|
1950
|
-
choices: Object.values(regions).map((region2) => ({
|
|
1951
|
-
name: regionNames[region2],
|
|
1952
|
-
value: region2
|
|
1953
|
-
})),
|
|
1954
|
-
default: regions.EU
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
spinner.start(`Logging in with token`);
|
|
1958
|
-
const user = await loginWithToken(userToken, userRegion);
|
|
1959
|
-
spinner.succeed();
|
|
1960
|
-
if (user) {
|
|
1961
|
-
updateSession(user.email, userToken, userRegion);
|
|
1962
|
-
await persistCredentials(userRegion);
|
|
1963
|
-
konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(user.friendly_name)}.`, true);
|
|
1964
|
-
}
|
|
1965
|
-
} else {
|
|
1966
|
-
const userEmail = await input({
|
|
1967
|
-
message: "Please enter your email address:",
|
|
1968
|
-
required: true,
|
|
1969
|
-
validate: (value) => {
|
|
1970
|
-
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
|
1971
|
-
return emailRegex.test(value);
|
|
1972
|
-
}
|
|
1973
|
-
});
|
|
1974
|
-
const userPassword = await password({
|
|
1975
|
-
message: "Please enter your password:"
|
|
1976
|
-
});
|
|
1977
|
-
let userRegion = region;
|
|
1978
|
-
if (!userRegion) {
|
|
1979
|
-
userRegion = await select({
|
|
1980
|
-
message: "Please select the region you would like to work in:",
|
|
1981
|
-
choices: Object.values(regions).map((region2) => ({
|
|
1982
|
-
name: regionNames[region2],
|
|
1983
|
-
value: region2
|
|
1984
|
-
})),
|
|
1985
|
-
default: regions.EU
|
|
1986
|
-
});
|
|
1987
|
-
}
|
|
1988
|
-
spinner.start(`Logging in with email`);
|
|
1989
|
-
spinner.succeed();
|
|
1990
|
-
const response = await loginWithEmailAndPassword(userEmail, userPassword, userRegion);
|
|
1991
|
-
if (response?.otp_required) {
|
|
1992
|
-
const otp = await input({
|
|
1993
|
-
message: "Add the code from your Authenticator app, or the one we sent to your e-mail / phone:",
|
|
1994
|
-
required: true
|
|
1995
|
-
});
|
|
1996
|
-
const otpResponse = await loginWithOtp(userEmail, userPassword, otp, userRegion);
|
|
1997
|
-
if (otpResponse?.access_token) {
|
|
1998
|
-
updateSession(userEmail, otpResponse?.access_token, userRegion);
|
|
1999
|
-
}
|
|
2000
|
-
} else if (response?.access_token) {
|
|
2001
|
-
updateSession(userEmail, response.access_token, userRegion);
|
|
2002
|
-
}
|
|
2003
|
-
await persistCredentials(region);
|
|
2004
|
-
konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(userEmail)}.`, true);
|
|
2095
|
+
const result = await performInteractiveLogin({
|
|
2096
|
+
verbose,
|
|
2097
|
+
preSelectedRegion: region,
|
|
2098
|
+
showWelcomeMessage: true
|
|
2099
|
+
});
|
|
2100
|
+
if (!result) {
|
|
2101
|
+
konsola.warn("Login cancelled or failed.");
|
|
2005
2102
|
}
|
|
2006
2103
|
} catch (error) {
|
|
2007
|
-
spinner.failed();
|
|
2008
2104
|
konsola.br();
|
|
2009
2105
|
handleError(error, verbose);
|
|
2010
2106
|
}
|
|
@@ -2012,10 +2108,10 @@ program$h.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
2012
2108
|
konsola.br();
|
|
2013
2109
|
});
|
|
2014
2110
|
|
|
2015
|
-
const program$
|
|
2016
|
-
program$
|
|
2111
|
+
const program$i = getProgram();
|
|
2112
|
+
program$i.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
2017
2113
|
konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
|
|
2018
|
-
const verbose = program$
|
|
2114
|
+
const verbose = program$i.opts().verbose;
|
|
2019
2115
|
try {
|
|
2020
2116
|
const { state, initializeSession } = session();
|
|
2021
2117
|
await initializeSession();
|
|
@@ -2063,10 +2159,10 @@ async function openSignupInBrowser(url) {
|
|
|
2063
2159
|
}
|
|
2064
2160
|
}
|
|
2065
2161
|
|
|
2066
|
-
const program$
|
|
2067
|
-
program$
|
|
2162
|
+
const program$h = getProgram();
|
|
2163
|
+
program$h.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
2068
2164
|
konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
|
|
2069
|
-
const verbose = program$
|
|
2165
|
+
const verbose = program$h.opts().verbose;
|
|
2070
2166
|
const { state, initializeSession } = session();
|
|
2071
2167
|
await initializeSession();
|
|
2072
2168
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -2088,10 +2184,10 @@ program$f.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
|
|
|
2088
2184
|
konsola.br();
|
|
2089
2185
|
});
|
|
2090
2186
|
|
|
2091
|
-
const program$
|
|
2092
|
-
program$
|
|
2187
|
+
const program$g = getProgram();
|
|
2188
|
+
program$g.command(commands.USER).description("Get the current user").action(async () => {
|
|
2093
2189
|
konsola.title(`${commands.USER}`, colorPalette.USER);
|
|
2094
|
-
const verbose = program$
|
|
2190
|
+
const verbose = program$g.opts().verbose;
|
|
2095
2191
|
const { state, initializeSession } = session();
|
|
2096
2192
|
await initializeSession();
|
|
2097
2193
|
if (!requireAuthentication(state)) {
|
|
@@ -2120,8 +2216,8 @@ program$e.command(commands.USER).description("Get the current user").action(asyn
|
|
|
2120
2216
|
konsola.br();
|
|
2121
2217
|
});
|
|
2122
2218
|
|
|
2123
|
-
const program$
|
|
2124
|
-
const componentsCommand = program$
|
|
2219
|
+
const program$f = getProgram();
|
|
2220
|
+
const componentsCommand = program$f.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/components");
|
|
2125
2221
|
|
|
2126
2222
|
const DEFAULT_COMPONENTS_FILENAME = "components";
|
|
2127
2223
|
const DEFAULT_GROUPS_FILENAME = "groups";
|
|
@@ -2506,10 +2602,10 @@ async function readConsolidatedFiles$1(resolvedPath, suffix) {
|
|
|
2506
2602
|
};
|
|
2507
2603
|
}
|
|
2508
2604
|
|
|
2509
|
-
const program$
|
|
2605
|
+
const program$e = getProgram();
|
|
2510
2606
|
componentsCommand.command("pull [componentName]").option("-f, --filename <filename>", "custom name to be used in file(s) name instead of space id").option("--sf, --separate-files", "Argument to create a single file for each component").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. components.<suffix>.json)").description(`Download your space's components schema as json. Optionally specify a component name to pull a single component.`).action(async (componentName, options) => {
|
|
2511
2607
|
konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
|
|
2512
|
-
const verbose = program$
|
|
2608
|
+
const verbose = program$e.opts().verbose;
|
|
2513
2609
|
const { space, path } = componentsCommand.opts();
|
|
2514
2610
|
const {
|
|
2515
2611
|
separateFiles = false,
|
|
@@ -3551,10 +3647,10 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = getAc
|
|
|
3551
3647
|
return results;
|
|
3552
3648
|
}
|
|
3553
3649
|
|
|
3554
|
-
const program$
|
|
3650
|
+
const program$d = getProgram();
|
|
3555
3651
|
componentsCommand.command("push [componentName]").description(`Push your space's components schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files", false).option("--su, --suffix <suffix>", "Suffix to add to the component name").action(async (componentName, options) => {
|
|
3556
3652
|
konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
|
|
3557
|
-
const verbose = program$
|
|
3653
|
+
const verbose = program$d.opts().verbose;
|
|
3558
3654
|
const { space, path } = componentsCommand.opts();
|
|
3559
3655
|
const { filter } = options;
|
|
3560
3656
|
const fromSpace = options.from || space;
|
|
@@ -3750,11 +3846,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
3750
3846
|
}
|
|
3751
3847
|
};
|
|
3752
3848
|
|
|
3753
|
-
const program$
|
|
3754
|
-
const languagesCommand = program$
|
|
3849
|
+
const program$c = getProgram();
|
|
3850
|
+
const languagesCommand = program$c.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/languages");
|
|
3755
3851
|
languagesCommand.command("pull").description(`Download your space's languages schema as json`).option("-f, --filename <filename>", "filename to save the file as <filename>.<suffix>.json").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. languages.<suffix>.json). By default, the space ID is used.").action(async (options) => {
|
|
3756
3852
|
konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
|
|
3757
|
-
const verbose = program$
|
|
3853
|
+
const verbose = program$c.opts().verbose;
|
|
3758
3854
|
const { space, path } = languagesCommand.opts();
|
|
3759
3855
|
const { filename = "languages", suffix = options.space } = options;
|
|
3760
3856
|
const { state, initializeSession } = session();
|
|
@@ -3801,8 +3897,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
|
|
|
3801
3897
|
konsola.br();
|
|
3802
3898
|
});
|
|
3803
3899
|
|
|
3804
|
-
const program$
|
|
3805
|
-
const migrationsCommand = program$
|
|
3900
|
+
const program$b = getProgram();
|
|
3901
|
+
const migrationsCommand = program$b.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/migrations");
|
|
3806
3902
|
|
|
3807
3903
|
const getMigrationTemplate = () => {
|
|
3808
3904
|
return `export default function (block) {
|
|
@@ -3920,11 +4016,29 @@ const fetchStory = async (spaceId, storyId) => {
|
|
|
3920
4016
|
},
|
|
3921
4017
|
throwOnError: true
|
|
3922
4018
|
});
|
|
3923
|
-
return data
|
|
4019
|
+
return data.story;
|
|
3924
4020
|
} catch (error) {
|
|
3925
4021
|
handleAPIError("pull_story", error);
|
|
3926
4022
|
}
|
|
3927
4023
|
};
|
|
4024
|
+
const createStory = async (spaceId, payload) => {
|
|
4025
|
+
try {
|
|
4026
|
+
const client = mapiClient();
|
|
4027
|
+
const { data } = await client.stories.create({
|
|
4028
|
+
path: {
|
|
4029
|
+
space_id: spaceId
|
|
4030
|
+
},
|
|
4031
|
+
body: {
|
|
4032
|
+
story: payload.story,
|
|
4033
|
+
publish: payload.publish
|
|
4034
|
+
},
|
|
4035
|
+
throwOnError: true
|
|
4036
|
+
});
|
|
4037
|
+
return data?.story;
|
|
4038
|
+
} catch (maybeError) {
|
|
4039
|
+
handleAPIError("create_story", toError(maybeError));
|
|
4040
|
+
}
|
|
4041
|
+
};
|
|
3928
4042
|
const updateStory = async (spaceId, storyId, payload) => {
|
|
3929
4043
|
try {
|
|
3930
4044
|
const client = mapiClient();
|
|
@@ -3940,9 +4054,14 @@ const updateStory = async (spaceId, storyId, payload) => {
|
|
|
3940
4054
|
},
|
|
3941
4055
|
throwOnError: true
|
|
3942
4056
|
});
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
4057
|
+
const { story } = data;
|
|
4058
|
+
if (!story) {
|
|
4059
|
+
throw new Error("Failed to update story");
|
|
4060
|
+
}
|
|
4061
|
+
return story;
|
|
4062
|
+
} catch (maybeError) {
|
|
4063
|
+
handleAPIError("update_story", toError(maybeError));
|
|
4064
|
+
throw maybeError;
|
|
3946
4065
|
}
|
|
3947
4066
|
};
|
|
3948
4067
|
|
|
@@ -4399,6 +4518,49 @@ const isStoryPublishedWithoutChanges = (story) => {
|
|
|
4399
4518
|
const isStoryWithUnpublishedChanges = (story) => {
|
|
4400
4519
|
return story.published && story.unpublished_changes;
|
|
4401
4520
|
};
|
|
4521
|
+
const toComponent = (maybeComponent) => {
|
|
4522
|
+
if (maybeComponent.component_group_uuid === void 0) {
|
|
4523
|
+
return null;
|
|
4524
|
+
}
|
|
4525
|
+
return maybeComponent;
|
|
4526
|
+
};
|
|
4527
|
+
const findComponentSchemas = async (directoryPath) => {
|
|
4528
|
+
const files = await readdir(directoryPath).catch((error) => {
|
|
4529
|
+
if (error.code === "ENOENT") {
|
|
4530
|
+
return [];
|
|
4531
|
+
}
|
|
4532
|
+
throw error;
|
|
4533
|
+
});
|
|
4534
|
+
const fileContents = files.filter((f) => path.extname(f) === ".json").map((f) => {
|
|
4535
|
+
const filePath = path.join(directoryPath, f);
|
|
4536
|
+
const fileContent = readFileSync(filePath, "utf-8");
|
|
4537
|
+
return JSON.parse(fileContent);
|
|
4538
|
+
});
|
|
4539
|
+
const components = [];
|
|
4540
|
+
for (const content of fileContents) {
|
|
4541
|
+
if (Array.isArray(content)) {
|
|
4542
|
+
for (const maybeComponent of content) {
|
|
4543
|
+
const component2 = toComponent(maybeComponent);
|
|
4544
|
+
if (component2) {
|
|
4545
|
+
components.push(component2);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
continue;
|
|
4549
|
+
}
|
|
4550
|
+
const component = toComponent(content);
|
|
4551
|
+
if (component) {
|
|
4552
|
+
components.push(component);
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
const schemas = {};
|
|
4556
|
+
for (const component of components) {
|
|
4557
|
+
schemas[component.name] = component.schema;
|
|
4558
|
+
}
|
|
4559
|
+
return schemas;
|
|
4560
|
+
};
|
|
4561
|
+
const getStoryFilename = (story) => {
|
|
4562
|
+
return `${story.slug}_${story.uuid}.json`;
|
|
4563
|
+
};
|
|
4402
4564
|
|
|
4403
4565
|
class UpdateStream extends Writable {
|
|
4404
4566
|
constructor(options) {
|
|
@@ -4738,8 +4900,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
|
|
|
4738
4900
|
}
|
|
4739
4901
|
});
|
|
4740
4902
|
|
|
4741
|
-
const program$
|
|
4742
|
-
const typesCommand = program$
|
|
4903
|
+
const program$a = getProgram();
|
|
4904
|
+
const typesCommand = program$a.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/types");
|
|
4743
4905
|
|
|
4744
4906
|
const getAssetJSONSchema = (title) => ({
|
|
4745
4907
|
$id: "#/asset",
|
|
@@ -5649,13 +5811,13 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
|
|
|
5649
5811
|
};
|
|
5650
5812
|
}
|
|
5651
5813
|
|
|
5652
|
-
const program$
|
|
5814
|
+
const program$9 = getProgram();
|
|
5653
5815
|
typesCommand.command("generate").description("Generate types d.ts for your component schemas").option(
|
|
5654
5816
|
"--filename <name>",
|
|
5655
5817
|
"Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
|
|
5656
5818
|
).option("--sf, --separate-files", "Generate one .d.ts file per component instead of a single combined file").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--type-suffix <suffix>", "suffix to be appended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").action(async (options) => {
|
|
5657
5819
|
konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
|
|
5658
|
-
const verbose = program$
|
|
5820
|
+
const verbose = program$9.opts().verbose;
|
|
5659
5821
|
const { space, path } = typesCommand.opts();
|
|
5660
5822
|
const spinner = new Spinner({
|
|
5661
5823
|
verbose: !isVitest
|
|
@@ -5708,8 +5870,8 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
|
|
|
5708
5870
|
}
|
|
5709
5871
|
});
|
|
5710
5872
|
|
|
5711
|
-
const program$
|
|
5712
|
-
const datasourcesCommand = program$
|
|
5873
|
+
const program$8 = getProgram();
|
|
5874
|
+
const datasourcesCommand = program$8.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/datasources");
|
|
5713
5875
|
|
|
5714
5876
|
async function fetchAllPages(fetchFunction, extractDataFunction, page = 1, collectedItems = []) {
|
|
5715
5877
|
const { data, response } = await fetchFunction(page);
|
|
@@ -5815,10 +5977,10 @@ const saveDatasourcesToFiles = async (space, datasources, options) => {
|
|
|
5815
5977
|
}
|
|
5816
5978
|
};
|
|
5817
5979
|
|
|
5818
|
-
const program$
|
|
5980
|
+
const program$7 = getProgram();
|
|
5819
5981
|
datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <filename>", "custom name to be used in file(s) name instead of space id").option("--sf, --separate-files", "Argument to create a single file for each datasource").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. datasources.<suffix>.json)").description("Pull datasources from your space").action(async (datasourceName, options) => {
|
|
5820
5982
|
konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pulling datasource ${datasourceName}...` : "Pulling datasources...");
|
|
5821
|
-
const verbose = program$
|
|
5983
|
+
const verbose = program$7.opts().verbose;
|
|
5822
5984
|
const { space, path } = datasourcesCommand.opts();
|
|
5823
5985
|
const {
|
|
5824
5986
|
separateFiles = false,
|
|
@@ -5896,10 +6058,10 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
|
|
|
5896
6058
|
}
|
|
5897
6059
|
});
|
|
5898
6060
|
|
|
5899
|
-
const program$
|
|
6061
|
+
const program$6 = getProgram();
|
|
5900
6062
|
datasourcesCommand.command("push [datasourceName]").description(`Push your space's datasources schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the datasources before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Suffix to add to the datasource name").action(async (datasourceName, options) => {
|
|
5901
6063
|
konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
|
|
5902
|
-
const verbose = program$
|
|
6064
|
+
const verbose = program$6.opts().verbose;
|
|
5903
6065
|
const { space, path } = datasourcesCommand.opts();
|
|
5904
6066
|
const { filter } = options;
|
|
5905
6067
|
const fromSpace = options.from || space;
|
|
@@ -6257,13 +6419,31 @@ function showNextSteps(technologyTemplate, finalProjectPath) {
|
|
|
6257
6419
|
`);
|
|
6258
6420
|
konsola.info(`Or check the dedicated guide at: ${chalk.hex(colorPalette.PRIMARY)(`https://www.storyblok.com/docs/guides/${technologyTemplate}`)}`);
|
|
6259
6421
|
}
|
|
6260
|
-
|
|
6261
|
-
|
|
6422
|
+
async function promptForLogin(verbose) {
|
|
6423
|
+
try {
|
|
6424
|
+
konsola.br();
|
|
6425
|
+
const shouldLogin = await confirm({
|
|
6426
|
+
message: "Would you like to login now?",
|
|
6427
|
+
default: true
|
|
6428
|
+
});
|
|
6429
|
+
if (!shouldLogin) {
|
|
6430
|
+
konsola.warn('Login cancelled. You can login later using the "storyblok login" command.');
|
|
6431
|
+
return null;
|
|
6432
|
+
}
|
|
6433
|
+
return await performInteractiveLogin({ verbose, showWelcomeMessage: true });
|
|
6434
|
+
} catch (error) {
|
|
6435
|
+
konsola.br();
|
|
6436
|
+
handleError(error, verbose);
|
|
6437
|
+
return null;
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
6440
|
+
const program$5 = getProgram();
|
|
6441
|
+
program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
|
|
6262
6442
|
"-r, --region <region>",
|
|
6263
6443
|
`The region to apply to the generated project template (does not affect space creation).`
|
|
6264
6444
|
).action(async (projectPath, options) => {
|
|
6265
6445
|
konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
|
|
6266
|
-
const verbose = program$
|
|
6446
|
+
const verbose = program$5.opts().verbose;
|
|
6267
6447
|
const { template, blueprint, token } = options;
|
|
6268
6448
|
if (options.region && !isRegion(options.region)) {
|
|
6269
6449
|
handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
|
|
@@ -6278,20 +6458,38 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
6278
6458
|
}
|
|
6279
6459
|
const { state, initializeSession } = session();
|
|
6280
6460
|
await initializeSession();
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6461
|
+
let password;
|
|
6462
|
+
let region;
|
|
6463
|
+
if (state.region) {
|
|
6464
|
+
region = state.region;
|
|
6465
|
+
}
|
|
6466
|
+
if (!token && !options.skipSpace) {
|
|
6467
|
+
if (!requireAuthentication(state, verbose)) {
|
|
6468
|
+
const loginResult = await promptForLogin(verbose);
|
|
6469
|
+
if (!loginResult) {
|
|
6470
|
+
return;
|
|
6471
|
+
}
|
|
6472
|
+
await initializeSession();
|
|
6473
|
+
}
|
|
6474
|
+
const authenticatedState = state;
|
|
6475
|
+
password = authenticatedState.password;
|
|
6476
|
+
region = authenticatedState.region;
|
|
6477
|
+
if (options.region && options.region !== region) {
|
|
6478
|
+
handleError(new CommandError(`Cannot create space in region "${options.region}". Your account is configured for region "${region}". Space creation must use your account's region.`));
|
|
6479
|
+
return;
|
|
6480
|
+
}
|
|
6481
|
+
mapiClient({
|
|
6482
|
+
token: {
|
|
6483
|
+
accessToken: password
|
|
6484
|
+
},
|
|
6485
|
+
region
|
|
6486
|
+
});
|
|
6487
|
+
} else if (state.isLoggedIn && state.password) {
|
|
6488
|
+
password = state.password;
|
|
6489
|
+
if (state.region) {
|
|
6490
|
+
region = state.region;
|
|
6491
|
+
}
|
|
6288
6492
|
}
|
|
6289
|
-
mapiClient({
|
|
6290
|
-
token: {
|
|
6291
|
-
accessToken: password
|
|
6292
|
-
},
|
|
6293
|
-
region
|
|
6294
|
-
});
|
|
6295
6493
|
const spinnerBlueprints = new Spinner({
|
|
6296
6494
|
verbose: !isVitest
|
|
6297
6495
|
});
|
|
@@ -6374,10 +6572,26 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
6374
6572
|
throw new Error("User data is undefined");
|
|
6375
6573
|
}
|
|
6376
6574
|
userData = user;
|
|
6377
|
-
} catch
|
|
6378
|
-
konsola.error("Failed to fetch user info.
|
|
6379
|
-
|
|
6380
|
-
|
|
6575
|
+
} catch {
|
|
6576
|
+
konsola.error("Failed to fetch user info. Your session may have expired.");
|
|
6577
|
+
const loginResult = await promptForLogin(verbose);
|
|
6578
|
+
if (!loginResult) {
|
|
6579
|
+
konsola.br();
|
|
6580
|
+
return;
|
|
6581
|
+
}
|
|
6582
|
+
await initializeSession();
|
|
6583
|
+
const { password: newPassword, region: newRegion } = session().state;
|
|
6584
|
+
try {
|
|
6585
|
+
const user = await getUser(newPassword, newRegion);
|
|
6586
|
+
if (!user) {
|
|
6587
|
+
throw new Error("User data is undefined");
|
|
6588
|
+
}
|
|
6589
|
+
userData = user;
|
|
6590
|
+
} catch (retryError) {
|
|
6591
|
+
konsola.error("Failed to fetch user info after login.", retryError);
|
|
6592
|
+
konsola.br();
|
|
6593
|
+
return;
|
|
6594
|
+
}
|
|
6381
6595
|
}
|
|
6382
6596
|
const choices = [
|
|
6383
6597
|
{ name: "My personal account", value: "personal" }
|
|
@@ -6455,13 +6669,13 @@ program$3.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
6455
6669
|
konsola.br();
|
|
6456
6670
|
});
|
|
6457
6671
|
|
|
6458
|
-
const program$
|
|
6459
|
-
const logsCommand = program$
|
|
6672
|
+
const program$4 = getProgram();
|
|
6673
|
+
const logsCommand = program$4.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`).option("-s, --space <space>", "The space ID.").option("-p, --path <path>", "Path to the directory containing the logs directory. Defaults to '.storyblok'.");
|
|
6460
6674
|
|
|
6461
6675
|
logsCommand.command("list").description("List logs").action(async () => {
|
|
6462
6676
|
const { space, path } = logsCommand.opts();
|
|
6463
6677
|
const ui = getUI();
|
|
6464
|
-
const logsPath = resolveCommandPath(directories.
|
|
6678
|
+
const logsPath = resolveCommandPath(directories.logs, space, path);
|
|
6465
6679
|
const logFiles = FileTransport.listLogFiles(logsPath);
|
|
6466
6680
|
if (logFiles.length === 0) {
|
|
6467
6681
|
ui.info(`No logs found for space "${space}".`);
|
|
@@ -6474,18 +6688,18 @@ logsCommand.command("list").description("List logs").action(async () => {
|
|
|
6474
6688
|
logsCommand.command("prune").description("Prune logs").option("--keep <number>", "Max number of log files to keep (default `0`, meaning remove all)", Number.parseInt, 0).action(async ({ keep }) => {
|
|
6475
6689
|
const { space, path } = logsCommand.opts();
|
|
6476
6690
|
const ui = getUI();
|
|
6477
|
-
const logsPath = resolveCommandPath(directories.
|
|
6691
|
+
const logsPath = resolveCommandPath(directories.logs, space, path);
|
|
6478
6692
|
const deletedFilesCount = FileTransport.pruneLogFiles(logsPath, keep);
|
|
6479
6693
|
ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
6480
6694
|
});
|
|
6481
6695
|
|
|
6482
|
-
const program$
|
|
6483
|
-
const reportsCommand = program$
|
|
6696
|
+
const program$3 = getProgram();
|
|
6697
|
+
const reportsCommand = program$3.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.").option("-s, --space <space>", "The space ID.").option("-p, --path <path>", "Path to the directory containing the reports directory. Defaults to '.storyblok'.");
|
|
6484
6698
|
|
|
6485
6699
|
reportsCommand.command("list").description("List reports").action(async () => {
|
|
6486
6700
|
const { space, path } = reportsCommand.opts();
|
|
6487
6701
|
const ui = getUI();
|
|
6488
|
-
const reportsPath = resolveCommandPath(directories.
|
|
6702
|
+
const reportsPath = resolveCommandPath(directories.reports, space, path);
|
|
6489
6703
|
const reportFiles = Reporter.listReportFiles(reportsPath, ".jsonl");
|
|
6490
6704
|
if (reportFiles.length === 0) {
|
|
6491
6705
|
ui.info(`No reports found for space "${space}".`);
|
|
@@ -6498,11 +6712,2248 @@ reportsCommand.command("list").description("List reports").action(async () => {
|
|
|
6498
6712
|
reportsCommand.command("prune").description("Prune reports").option("--keep <number>", "Max number of report files to keep (default `0`, meaning remove all)", Number.parseInt, 0).action(async ({ keep }) => {
|
|
6499
6713
|
const { space, path } = reportsCommand.opts();
|
|
6500
6714
|
const ui = getUI();
|
|
6501
|
-
const reportsPath = resolveCommandPath(directories.
|
|
6715
|
+
const reportsPath = resolveCommandPath(directories.reports, space, path);
|
|
6502
6716
|
const deletedFilesCount = Reporter.pruneReportFiles(reportsPath, keep, ".jsonl");
|
|
6503
6717
|
ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
6504
6718
|
});
|
|
6505
6719
|
|
|
6720
|
+
const program$2 = getProgram();
|
|
6721
|
+
const assetsCommand = program$2.command(commands.ASSETS).description(`Manage your space's assets`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "base path to store assets (default .storyblok)");
|
|
6722
|
+
|
|
6723
|
+
const fetchAssets = async ({ spaceId, params }) => {
|
|
6724
|
+
try {
|
|
6725
|
+
const client = mapiClient();
|
|
6726
|
+
const { data, response } = await client.assets.list({
|
|
6727
|
+
path: {
|
|
6728
|
+
space_id: spaceId
|
|
6729
|
+
},
|
|
6730
|
+
query: {
|
|
6731
|
+
...params,
|
|
6732
|
+
per_page: params?.per_page || 100,
|
|
6733
|
+
page: params?.page || 1
|
|
6734
|
+
},
|
|
6735
|
+
throwOnError: true
|
|
6736
|
+
});
|
|
6737
|
+
const assets = (data?.assets || []).filter((asset) => Boolean(asset?.id && asset?.filename));
|
|
6738
|
+
return {
|
|
6739
|
+
assets,
|
|
6740
|
+
headers: response.headers
|
|
6741
|
+
};
|
|
6742
|
+
} catch (maybeError) {
|
|
6743
|
+
handleAPIError("pull_assets", toError(maybeError));
|
|
6744
|
+
throw maybeError;
|
|
6745
|
+
}
|
|
6746
|
+
};
|
|
6747
|
+
const downloadFile = async (filename) => {
|
|
6748
|
+
const response = await fetch(filename);
|
|
6749
|
+
if (!response.ok) {
|
|
6750
|
+
throw new Error(`Failed to download ${filename}`);
|
|
6751
|
+
}
|
|
6752
|
+
return response.arrayBuffer();
|
|
6753
|
+
};
|
|
6754
|
+
const getSignedAssetUrl = async (filename, assetToken, region) => {
|
|
6755
|
+
try {
|
|
6756
|
+
const client = new Storyblok({
|
|
6757
|
+
accessToken: assetToken,
|
|
6758
|
+
region: region || "eu"
|
|
6759
|
+
});
|
|
6760
|
+
const response = await client.get("cdn/assets/me", {
|
|
6761
|
+
filename
|
|
6762
|
+
});
|
|
6763
|
+
return response.data.asset.signed_url;
|
|
6764
|
+
} catch (maybeError) {
|
|
6765
|
+
handleAPIError("pull_asset", toError(maybeError));
|
|
6766
|
+
throw maybeError;
|
|
6767
|
+
}
|
|
6768
|
+
};
|
|
6769
|
+
const fetchAssetFolders = async ({ spaceId }) => {
|
|
6770
|
+
try {
|
|
6771
|
+
const client = mapiClient();
|
|
6772
|
+
const { data, response } = await client.assetFolders.list({
|
|
6773
|
+
path: {
|
|
6774
|
+
space_id: spaceId
|
|
6775
|
+
},
|
|
6776
|
+
throwOnError: true
|
|
6777
|
+
});
|
|
6778
|
+
return {
|
|
6779
|
+
asset_folders: data.asset_folders || [],
|
|
6780
|
+
headers: response.headers
|
|
6781
|
+
};
|
|
6782
|
+
} catch (maybeError) {
|
|
6783
|
+
handleAPIError("pull_asset_folders", toError(maybeError));
|
|
6784
|
+
throw maybeError;
|
|
6785
|
+
}
|
|
6786
|
+
};
|
|
6787
|
+
const createAssetFolder = async (folder, {
|
|
6788
|
+
spaceId
|
|
6789
|
+
}) => {
|
|
6790
|
+
try {
|
|
6791
|
+
const client = mapiClient();
|
|
6792
|
+
const { data } = await client.assetFolders.create({
|
|
6793
|
+
path: {
|
|
6794
|
+
space_id: spaceId
|
|
6795
|
+
},
|
|
6796
|
+
body: { asset_folder: folder },
|
|
6797
|
+
throwOnError: true
|
|
6798
|
+
});
|
|
6799
|
+
const { asset_folder } = data;
|
|
6800
|
+
if (!asset_folder) {
|
|
6801
|
+
throw new Error("Failed to create asset folder");
|
|
6802
|
+
}
|
|
6803
|
+
return asset_folder;
|
|
6804
|
+
} catch (maybeError) {
|
|
6805
|
+
handleAPIError("push_asset_folder", toError(maybeError));
|
|
6806
|
+
throw maybeError;
|
|
6807
|
+
}
|
|
6808
|
+
};
|
|
6809
|
+
const updateAssetFolder = async (folder, {
|
|
6810
|
+
spaceId
|
|
6811
|
+
}) => {
|
|
6812
|
+
try {
|
|
6813
|
+
const client = mapiClient();
|
|
6814
|
+
await client.assetFolders.update({
|
|
6815
|
+
path: {
|
|
6816
|
+
asset_folder_id: folder.id,
|
|
6817
|
+
space_id: spaceId
|
|
6818
|
+
},
|
|
6819
|
+
body: { asset_folder: folder },
|
|
6820
|
+
throwOnError: true
|
|
6821
|
+
});
|
|
6822
|
+
return folder;
|
|
6823
|
+
} catch (maybeError) {
|
|
6824
|
+
handleAPIError("push_asset_folder", toError(maybeError));
|
|
6825
|
+
throw maybeError;
|
|
6826
|
+
}
|
|
6827
|
+
};
|
|
6828
|
+
const requestAssetUpload = async (asset, { spaceId }) => {
|
|
6829
|
+
try {
|
|
6830
|
+
const client = mapiClient();
|
|
6831
|
+
const { data } = await client.assets.upload({
|
|
6832
|
+
path: {
|
|
6833
|
+
space_id: spaceId
|
|
6834
|
+
},
|
|
6835
|
+
body: {
|
|
6836
|
+
// @ts-expect-error Our types are wrong, id is optional but allowed.
|
|
6837
|
+
id: asset.id,
|
|
6838
|
+
filename: asset.short_filename,
|
|
6839
|
+
asset_folder_id: asset.asset_folder_id ?? void 0,
|
|
6840
|
+
is_private: asset.is_private
|
|
6841
|
+
},
|
|
6842
|
+
throwOnError: true
|
|
6843
|
+
});
|
|
6844
|
+
const signedUpload = data;
|
|
6845
|
+
if (!signedUpload?.id || !signedUpload?.post_url || !signedUpload?.fields) {
|
|
6846
|
+
throw new Error("Failed to request signed upload!");
|
|
6847
|
+
}
|
|
6848
|
+
return signedUpload;
|
|
6849
|
+
} catch (maybeError) {
|
|
6850
|
+
handleAPIError("push_asset_sign", toError(maybeError));
|
|
6851
|
+
throw maybeError;
|
|
6852
|
+
}
|
|
6853
|
+
};
|
|
6854
|
+
const uploadAssetToS3 = async (asset, fileBuffer, {
|
|
6855
|
+
signedUpload
|
|
6856
|
+
}) => {
|
|
6857
|
+
if (!signedUpload?.id || !signedUpload?.post_url || !signedUpload?.fields) {
|
|
6858
|
+
throw new Error("Invalid signed upload!");
|
|
6859
|
+
}
|
|
6860
|
+
const formData = new FormData();
|
|
6861
|
+
for (const [key, value] of Object.entries(signedUpload.fields)) {
|
|
6862
|
+
formData.append(key, value);
|
|
6863
|
+
}
|
|
6864
|
+
const contentType = signedUpload.fields["Content-Type"] || "application/octet-stream";
|
|
6865
|
+
formData.append("file", new File([Buffer.from(fileBuffer)], asset.short_filename, { type: contentType }));
|
|
6866
|
+
const response = await fetch(signedUpload.post_url, {
|
|
6867
|
+
method: "POST",
|
|
6868
|
+
body: formData
|
|
6869
|
+
});
|
|
6870
|
+
if (!response.ok) {
|
|
6871
|
+
handleAPIError("push_asset_upload", new Error("Failed to upload asset to storage"));
|
|
6872
|
+
return;
|
|
6873
|
+
}
|
|
6874
|
+
return response;
|
|
6875
|
+
};
|
|
6876
|
+
const finishAssetUpload = async (assetId, {
|
|
6877
|
+
spaceId
|
|
6878
|
+
}) => {
|
|
6879
|
+
try {
|
|
6880
|
+
const client = mapiClient();
|
|
6881
|
+
await client.assets.finalize({
|
|
6882
|
+
path: {
|
|
6883
|
+
space_id: spaceId,
|
|
6884
|
+
signed_response_object_id: String(assetId)
|
|
6885
|
+
},
|
|
6886
|
+
throwOnError: true
|
|
6887
|
+
});
|
|
6888
|
+
const { data } = await client.assets.get({
|
|
6889
|
+
path: {
|
|
6890
|
+
space_id: spaceId,
|
|
6891
|
+
asset_id: assetId
|
|
6892
|
+
},
|
|
6893
|
+
throwOnError: true
|
|
6894
|
+
});
|
|
6895
|
+
return data;
|
|
6896
|
+
} catch (maybeError) {
|
|
6897
|
+
handleAPIError("push_asset_finish", toError(maybeError));
|
|
6898
|
+
throw maybeError;
|
|
6899
|
+
}
|
|
6900
|
+
};
|
|
6901
|
+
const uploadAsset = async (asset, fileBuffer, { spaceId }) => {
|
|
6902
|
+
const signed = await requestAssetUpload(asset, {
|
|
6903
|
+
spaceId
|
|
6904
|
+
});
|
|
6905
|
+
const uploadResponse = await uploadAssetToS3(asset, fileBuffer, {
|
|
6906
|
+
signedUpload: signed
|
|
6907
|
+
});
|
|
6908
|
+
if (!uploadResponse?.ok) {
|
|
6909
|
+
throw new Error("Error uploading asset to S3!");
|
|
6910
|
+
}
|
|
6911
|
+
return finishAssetUpload(Number(signed.id), {
|
|
6912
|
+
spaceId
|
|
6913
|
+
});
|
|
6914
|
+
};
|
|
6915
|
+
const sha256 = (data) => {
|
|
6916
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
6917
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
6918
|
+
};
|
|
6919
|
+
const downloadAssetFile = async (asset, options) => {
|
|
6920
|
+
let url = asset.filename;
|
|
6921
|
+
if (asset.is_private) {
|
|
6922
|
+
if (!options.assetToken) {
|
|
6923
|
+
throw new Error(`Asset ${asset.filename} is private but no asset token was provided. Use --asset-token to provide a token.`);
|
|
6924
|
+
}
|
|
6925
|
+
url = await getSignedAssetUrl(asset.filename, options.assetToken, options.region);
|
|
6926
|
+
}
|
|
6927
|
+
return downloadFile(url);
|
|
6928
|
+
};
|
|
6929
|
+
const updateAsset = async (asset, fileBuffer, {
|
|
6930
|
+
spaceId
|
|
6931
|
+
}) => {
|
|
6932
|
+
try {
|
|
6933
|
+
const assetWithNewFilename = { ...asset };
|
|
6934
|
+
if (fileBuffer) {
|
|
6935
|
+
const uploadedAsset = await uploadAsset({
|
|
6936
|
+
id: asset.id,
|
|
6937
|
+
asset_folder_id: asset.asset_folder_id,
|
|
6938
|
+
short_filename: asset.short_filename || basename(asset.filename)
|
|
6939
|
+
}, fileBuffer, { spaceId });
|
|
6940
|
+
assetWithNewFilename.filename = uploadedAsset.filename;
|
|
6941
|
+
assetWithNewFilename.short_filename = uploadedAsset.short_filename;
|
|
6942
|
+
}
|
|
6943
|
+
const client = mapiClient();
|
|
6944
|
+
await client.assets.update({
|
|
6945
|
+
path: {
|
|
6946
|
+
space_id: spaceId,
|
|
6947
|
+
asset_id: assetWithNewFilename.id
|
|
6948
|
+
},
|
|
6949
|
+
body: {
|
|
6950
|
+
asset: assetWithNewFilename
|
|
6951
|
+
},
|
|
6952
|
+
throwOnError: true
|
|
6953
|
+
});
|
|
6954
|
+
return assetWithNewFilename;
|
|
6955
|
+
} catch (maybeError) {
|
|
6956
|
+
handleAPIError("push_asset_update", toError(maybeError));
|
|
6957
|
+
throw maybeError;
|
|
6958
|
+
}
|
|
6959
|
+
};
|
|
6960
|
+
const createAsset = async (asset, fileBuffer, { spaceId }) => {
|
|
6961
|
+
const createdAsset = await uploadAsset({
|
|
6962
|
+
asset_folder_id: asset.asset_folder_id,
|
|
6963
|
+
short_filename: asset.short_filename,
|
|
6964
|
+
alt: asset.alt,
|
|
6965
|
+
title: asset.title,
|
|
6966
|
+
copyright: asset.copyright,
|
|
6967
|
+
source: asset.source,
|
|
6968
|
+
is_private: asset.is_private
|
|
6969
|
+
}, fileBuffer, { spaceId });
|
|
6970
|
+
const hasUpdatableMetadata = Boolean(
|
|
6971
|
+
asset.alt || asset.title || asset.copyright || asset.source || asset.is_private || asset.meta_data && Object.keys(asset.meta_data).length > 0
|
|
6972
|
+
);
|
|
6973
|
+
if (hasUpdatableMetadata) {
|
|
6974
|
+
const updatedAsset = await updateAsset({
|
|
6975
|
+
...asset,
|
|
6976
|
+
id: createdAsset.id,
|
|
6977
|
+
filename: createdAsset.filename
|
|
6978
|
+
}, null, {
|
|
6979
|
+
spaceId
|
|
6980
|
+
});
|
|
6981
|
+
if (!updatedAsset) {
|
|
6982
|
+
throw new Error("Updating the created asset failed!");
|
|
6983
|
+
}
|
|
6984
|
+
return updatedAsset;
|
|
6985
|
+
}
|
|
6986
|
+
return createdAsset;
|
|
6987
|
+
};
|
|
6988
|
+
|
|
6989
|
+
const parseAssetData = (raw) => {
|
|
6990
|
+
if (!raw) {
|
|
6991
|
+
return {};
|
|
6992
|
+
}
|
|
6993
|
+
try {
|
|
6994
|
+
const parsed = JSON.parse(raw);
|
|
6995
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6996
|
+
throw new Error("Asset data must be a JSON object.");
|
|
6997
|
+
}
|
|
6998
|
+
return parsed;
|
|
6999
|
+
} catch (maybeError) {
|
|
7000
|
+
throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
|
|
7001
|
+
}
|
|
7002
|
+
};
|
|
7003
|
+
const getSidecarFilename = (assetBinaryPath) => {
|
|
7004
|
+
return join(dirname$1(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
|
|
7005
|
+
};
|
|
7006
|
+
const loadSidecarAssetData = async (assetBinaryPath) => {
|
|
7007
|
+
const sidecarPath = getSidecarFilename(assetBinaryPath);
|
|
7008
|
+
try {
|
|
7009
|
+
const sidecarRaw = await readFile$1(sidecarPath, "utf8");
|
|
7010
|
+
try {
|
|
7011
|
+
return parseAssetData(sidecarRaw);
|
|
7012
|
+
} catch (maybeError) {
|
|
7013
|
+
throw new Error(`Invalid sidecar JSON: ${toError(maybeError).message}`);
|
|
7014
|
+
}
|
|
7015
|
+
} catch (maybeError) {
|
|
7016
|
+
const error = toError(maybeError);
|
|
7017
|
+
if (error.code === "ENOENT") {
|
|
7018
|
+
return {};
|
|
7019
|
+
}
|
|
7020
|
+
throw new Error(`Failed to read sidecar asset data: ${error.message}`);
|
|
7021
|
+
}
|
|
7022
|
+
};
|
|
7023
|
+
const isRemoteSource = (assetBinaryPath) => {
|
|
7024
|
+
try {
|
|
7025
|
+
const url = new URL(assetBinaryPath);
|
|
7026
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
7027
|
+
} catch {
|
|
7028
|
+
return false;
|
|
7029
|
+
}
|
|
7030
|
+
};
|
|
7031
|
+
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.old_filename && entry.new_filename);
|
|
7032
|
+
const loadAssetMap = async (manifestFile) => {
|
|
7033
|
+
const manifest = await loadManifest(manifestFile);
|
|
7034
|
+
return new Map([
|
|
7035
|
+
...manifest.filter(isValidManifestEntry).map((e) => [
|
|
7036
|
+
Number(e.old_id),
|
|
7037
|
+
{
|
|
7038
|
+
old: { id: Number(e.old_id), filename: e.old_filename || "" },
|
|
7039
|
+
new: { id: Number(e.new_id), filename: e.new_filename || "" }
|
|
7040
|
+
}
|
|
7041
|
+
])
|
|
7042
|
+
]);
|
|
7043
|
+
};
|
|
7044
|
+
const loadAssetFolderMap = async (manifestFile) => {
|
|
7045
|
+
const manifest = await loadManifest(manifestFile);
|
|
7046
|
+
return new Map(manifest.map((e) => [Number(e.old_id), Number(e.new_id)]));
|
|
7047
|
+
};
|
|
7048
|
+
const getAssetNameAndExt = (asset) => {
|
|
7049
|
+
const filename = asset.short_filename || (asset.filename ? basename(asset.filename) : void 0);
|
|
7050
|
+
if (!filename) {
|
|
7051
|
+
throw new Error(`Filename for asset with id ${asset.id} could not be determined!`);
|
|
7052
|
+
}
|
|
7053
|
+
const ext = extname(filename);
|
|
7054
|
+
const name = sanitizeFilename(filename.replace(ext, ""));
|
|
7055
|
+
return { name, ext };
|
|
7056
|
+
};
|
|
7057
|
+
const getAssetFilename = (asset) => {
|
|
7058
|
+
const { name } = getAssetNameAndExt(asset);
|
|
7059
|
+
return `${name}_${asset.id}.json`;
|
|
7060
|
+
};
|
|
7061
|
+
const getAssetBinaryFilename = (asset) => {
|
|
7062
|
+
const { name, ext } = getAssetNameAndExt(asset);
|
|
7063
|
+
return `${name}_${asset.id}${ext}`;
|
|
7064
|
+
};
|
|
7065
|
+
const getFolderFilename = (folder) => {
|
|
7066
|
+
const sanitizedName = sanitizeFilename(folder.name || "");
|
|
7067
|
+
const baseName = sanitizedName || folder.uuid;
|
|
7068
|
+
return `${baseName}_${folder.uuid}.json`;
|
|
7069
|
+
};
|
|
7070
|
+
|
|
7071
|
+
const apiConcurrencyLock$1 = new Sema(12);
|
|
7072
|
+
const fetchAssetsStream = ({
|
|
7073
|
+
spaceId,
|
|
7074
|
+
params = {},
|
|
7075
|
+
setTotalAssets,
|
|
7076
|
+
setTotalPages,
|
|
7077
|
+
onIncrement,
|
|
7078
|
+
onPageSuccess,
|
|
7079
|
+
onPageError
|
|
7080
|
+
}) => {
|
|
7081
|
+
const listGenerator = async function* assetListIterator() {
|
|
7082
|
+
let perPage = 100;
|
|
7083
|
+
let page = 1;
|
|
7084
|
+
let totalPages = 1;
|
|
7085
|
+
setTotalPages?.(totalPages);
|
|
7086
|
+
while (page <= totalPages) {
|
|
7087
|
+
try {
|
|
7088
|
+
const result = await fetchAssets({
|
|
7089
|
+
spaceId,
|
|
7090
|
+
params: {
|
|
7091
|
+
...params,
|
|
7092
|
+
per_page: perPage,
|
|
7093
|
+
page
|
|
7094
|
+
}
|
|
7095
|
+
});
|
|
7096
|
+
const { headers, assets } = result;
|
|
7097
|
+
const total = Number(headers.get("Total"));
|
|
7098
|
+
perPage = Number(headers.get("Per-Page")) || perPage;
|
|
7099
|
+
totalPages = Math.max(1, Math.ceil(total / perPage));
|
|
7100
|
+
setTotalAssets?.(total);
|
|
7101
|
+
setTotalPages?.(totalPages);
|
|
7102
|
+
onPageSuccess?.(page, totalPages);
|
|
7103
|
+
for (const asset of assets) {
|
|
7104
|
+
yield asset;
|
|
7105
|
+
}
|
|
7106
|
+
page += 1;
|
|
7107
|
+
} catch (maybeError) {
|
|
7108
|
+
onPageError?.(toError(maybeError), page, totalPages);
|
|
7109
|
+
break;
|
|
7110
|
+
} finally {
|
|
7111
|
+
onIncrement?.();
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
};
|
|
7115
|
+
return Readable.from(listGenerator());
|
|
7116
|
+
};
|
|
7117
|
+
const downloadAssetStream = ({
|
|
7118
|
+
assetToken,
|
|
7119
|
+
region,
|
|
7120
|
+
onIncrement,
|
|
7121
|
+
onAssetSuccess,
|
|
7122
|
+
onAssetError
|
|
7123
|
+
}) => {
|
|
7124
|
+
const processing = /* @__PURE__ */ new Set();
|
|
7125
|
+
return new Transform({
|
|
7126
|
+
objectMode: true,
|
|
7127
|
+
async transform(asset, _encoding, callback) {
|
|
7128
|
+
await apiConcurrencyLock$1.acquire();
|
|
7129
|
+
const task = downloadAssetFile(asset, { assetToken, region }).then((fileBuffer) => {
|
|
7130
|
+
if (!fileBuffer) {
|
|
7131
|
+
throw new Error("Invalid asset file!");
|
|
7132
|
+
}
|
|
7133
|
+
onAssetSuccess?.(asset);
|
|
7134
|
+
this.push({ asset, fileBuffer });
|
|
7135
|
+
}).catch((maybeError) => {
|
|
7136
|
+
onAssetError?.(toError(maybeError), asset);
|
|
7137
|
+
}).finally(() => {
|
|
7138
|
+
onIncrement?.();
|
|
7139
|
+
apiConcurrencyLock$1.release();
|
|
7140
|
+
processing.delete(task);
|
|
7141
|
+
});
|
|
7142
|
+
processing.add(task);
|
|
7143
|
+
callback();
|
|
7144
|
+
},
|
|
7145
|
+
flush(callback) {
|
|
7146
|
+
Promise.allSettled(processing).finally(() => callback());
|
|
7147
|
+
}
|
|
7148
|
+
});
|
|
7149
|
+
};
|
|
7150
|
+
const makeWriteAssetFSTransport = ({ directoryPath }) => async (asset, fileBuffer) => {
|
|
7151
|
+
const assetBinaryPath = join(directoryPath, getAssetBinaryFilename(asset));
|
|
7152
|
+
const assetPath = join(directoryPath, getAssetFilename(asset));
|
|
7153
|
+
await saveToFile(assetBinaryPath, Buffer.from(fileBuffer));
|
|
7154
|
+
await saveToFile(assetPath, JSON.stringify(asset, null, 2));
|
|
7155
|
+
return asset;
|
|
7156
|
+
};
|
|
7157
|
+
const writeAssetStream = ({
|
|
7158
|
+
writeAsset,
|
|
7159
|
+
onIncrement,
|
|
7160
|
+
onAssetSuccess,
|
|
7161
|
+
onAssetError
|
|
7162
|
+
}) => {
|
|
7163
|
+
const processing = /* @__PURE__ */ new Set();
|
|
7164
|
+
return new Writable({
|
|
7165
|
+
objectMode: true,
|
|
7166
|
+
async write(payload, _encoding, callback) {
|
|
7167
|
+
await apiConcurrencyLock$1.acquire();
|
|
7168
|
+
const task = (async () => {
|
|
7169
|
+
try {
|
|
7170
|
+
await writeAsset(payload.asset, payload.fileBuffer);
|
|
7171
|
+
onAssetSuccess?.(payload.asset);
|
|
7172
|
+
} catch (maybeError) {
|
|
7173
|
+
onAssetError?.(toError(maybeError), payload.asset);
|
|
7174
|
+
}
|
|
7175
|
+
})();
|
|
7176
|
+
processing.add(task);
|
|
7177
|
+
task.finally(() => {
|
|
7178
|
+
onIncrement?.();
|
|
7179
|
+
apiConcurrencyLock$1.release();
|
|
7180
|
+
processing.delete(task);
|
|
7181
|
+
});
|
|
7182
|
+
callback();
|
|
7183
|
+
},
|
|
7184
|
+
final(callback) {
|
|
7185
|
+
Promise.all(processing).finally(() => callback());
|
|
7186
|
+
}
|
|
7187
|
+
});
|
|
7188
|
+
};
|
|
7189
|
+
const fetchAssetFoldersStream = ({
|
|
7190
|
+
spaceId,
|
|
7191
|
+
setTotalFolders,
|
|
7192
|
+
onSuccess,
|
|
7193
|
+
onError
|
|
7194
|
+
}) => {
|
|
7195
|
+
const listGenerator = async function* folderListIterator() {
|
|
7196
|
+
try {
|
|
7197
|
+
const result = await fetchAssetFolders({ spaceId });
|
|
7198
|
+
const { asset_folders } = result;
|
|
7199
|
+
const total = asset_folders.length;
|
|
7200
|
+
setTotalFolders?.(total);
|
|
7201
|
+
onSuccess?.(asset_folders);
|
|
7202
|
+
for (const folder of asset_folders) {
|
|
7203
|
+
yield folder;
|
|
7204
|
+
}
|
|
7205
|
+
} catch (maybeError) {
|
|
7206
|
+
onError?.(toError(maybeError));
|
|
7207
|
+
}
|
|
7208
|
+
};
|
|
7209
|
+
return Readable.from(listGenerator());
|
|
7210
|
+
};
|
|
7211
|
+
const makeWriteAssetFolderFSTransport = ({ directoryPath }) => async (folder) => {
|
|
7212
|
+
const filename = getFolderFilename(folder);
|
|
7213
|
+
await saveToFile(join(directoryPath, "folders", filename), JSON.stringify(folder, null, 2));
|
|
7214
|
+
return folder;
|
|
7215
|
+
};
|
|
7216
|
+
const writeAssetFolderStream = ({
|
|
7217
|
+
writeAssetFolder,
|
|
7218
|
+
onIncrement,
|
|
7219
|
+
onFolderSuccess,
|
|
7220
|
+
onFolderError
|
|
7221
|
+
}) => {
|
|
7222
|
+
const processing = /* @__PURE__ */ new Set();
|
|
7223
|
+
return new Writable({
|
|
7224
|
+
objectMode: true,
|
|
7225
|
+
async write(folder, _encoding, callback) {
|
|
7226
|
+
await apiConcurrencyLock$1.acquire();
|
|
7227
|
+
const task = (async () => {
|
|
7228
|
+
try {
|
|
7229
|
+
await writeAssetFolder(folder);
|
|
7230
|
+
onFolderSuccess?.(folder);
|
|
7231
|
+
} catch (maybeError) {
|
|
7232
|
+
onFolderError?.(toError(maybeError), folder);
|
|
7233
|
+
}
|
|
7234
|
+
})();
|
|
7235
|
+
processing.add(task);
|
|
7236
|
+
task.finally(() => {
|
|
7237
|
+
onIncrement?.();
|
|
7238
|
+
apiConcurrencyLock$1.release();
|
|
7239
|
+
processing.delete(task);
|
|
7240
|
+
});
|
|
7241
|
+
callback();
|
|
7242
|
+
},
|
|
7243
|
+
final(callback) {
|
|
7244
|
+
Promise.all(processing).finally(() => callback());
|
|
7245
|
+
}
|
|
7246
|
+
});
|
|
7247
|
+
};
|
|
7248
|
+
const readLocalAssetFoldersStream = ({
|
|
7249
|
+
directoryPath,
|
|
7250
|
+
setTotalFolders,
|
|
7251
|
+
onFolderError
|
|
7252
|
+
}) => {
|
|
7253
|
+
const iterator = async function* readFolders() {
|
|
7254
|
+
try {
|
|
7255
|
+
const files = await readdir(directoryPath);
|
|
7256
|
+
const jsonFiles = new Set(files.filter((file) => file.endsWith(".json")));
|
|
7257
|
+
setTotalFolders?.(jsonFiles.size);
|
|
7258
|
+
const processed = /* @__PURE__ */ new Set();
|
|
7259
|
+
let maxIterations = jsonFiles.size * jsonFiles.size;
|
|
7260
|
+
while (jsonFiles.size > 0 && maxIterations-- > 0) {
|
|
7261
|
+
for (const file of jsonFiles) {
|
|
7262
|
+
try {
|
|
7263
|
+
const filePath = join(directoryPath, file);
|
|
7264
|
+
const content = await readFile$1(filePath, "utf8");
|
|
7265
|
+
const folder = JSON.parse(content);
|
|
7266
|
+
jsonFiles.delete(file);
|
|
7267
|
+
if (!folder.parent_id || processed.has(folder.parent_id)) {
|
|
7268
|
+
processed.add(folder.id);
|
|
7269
|
+
yield {
|
|
7270
|
+
folder,
|
|
7271
|
+
context: {
|
|
7272
|
+
localFilePath: filePath
|
|
7273
|
+
}
|
|
7274
|
+
};
|
|
7275
|
+
} else {
|
|
7276
|
+
jsonFiles.add(file);
|
|
7277
|
+
}
|
|
7278
|
+
} catch (maybeError) {
|
|
7279
|
+
onFolderError?.(toError(maybeError));
|
|
7280
|
+
}
|
|
7281
|
+
}
|
|
7282
|
+
}
|
|
7283
|
+
if (jsonFiles.size > 0) {
|
|
7284
|
+
onFolderError?.(new Error(`Unable to resolve folder dependencies for: ${[...jsonFiles].join(", ")}`));
|
|
7285
|
+
}
|
|
7286
|
+
} catch (maybeError) {
|
|
7287
|
+
const error = toError(maybeError);
|
|
7288
|
+
if ("code" in error && error.code === "ENOENT") {
|
|
7289
|
+
return;
|
|
7290
|
+
}
|
|
7291
|
+
onFolderError?.(error);
|
|
7292
|
+
}
|
|
7293
|
+
};
|
|
7294
|
+
return Readable.from(iterator());
|
|
7295
|
+
};
|
|
7296
|
+
const makeCreateAssetFolderAPITransport = ({ spaceId }) => (folder) => createAssetFolder({
|
|
7297
|
+
name: folder.name,
|
|
7298
|
+
parent_id: folder.parent_id ?? void 0
|
|
7299
|
+
}, {
|
|
7300
|
+
spaceId
|
|
7301
|
+
});
|
|
7302
|
+
const makeUpdateAssetFolderAPITransport = ({ spaceId }) => (folder) => updateAssetFolder(folder, { spaceId });
|
|
7303
|
+
const makeGetAssetFolderAPITransport = ({ spaceId }) => async (folderId) => {
|
|
7304
|
+
const { data, response } = await mapiClient().assetFolders.get({
|
|
7305
|
+
path: {
|
|
7306
|
+
asset_folder_id: folderId,
|
|
7307
|
+
space_id: spaceId
|
|
7308
|
+
}
|
|
7309
|
+
});
|
|
7310
|
+
if (!response.ok && response.status !== 404) {
|
|
7311
|
+
handleAPIError("pull_asset_folder", new FetchError(response.statusText, response));
|
|
7312
|
+
}
|
|
7313
|
+
return data?.asset_folder;
|
|
7314
|
+
};
|
|
7315
|
+
const makeCleanupAssetFolderFSTransport = () => async ({ localFilePath }) => await unlink(localFilePath);
|
|
7316
|
+
const upsertAssetFolderStream = ({
|
|
7317
|
+
transports,
|
|
7318
|
+
maps,
|
|
7319
|
+
onIncrement,
|
|
7320
|
+
onFolderSuccess,
|
|
7321
|
+
onFolderError
|
|
7322
|
+
}) => {
|
|
7323
|
+
return new Writable({
|
|
7324
|
+
objectMode: true,
|
|
7325
|
+
async write({ folder, context }, _encoding, callback) {
|
|
7326
|
+
try {
|
|
7327
|
+
const remoteParentId = folder.parent_id && (maps.assetFolders.get(folder.parent_id) || folder.parent_id);
|
|
7328
|
+
const remoteFolderId = maps.assetFolders.get(folder.id) || folder.id;
|
|
7329
|
+
const upsertFolder = {
|
|
7330
|
+
...folder,
|
|
7331
|
+
id: remoteFolderId,
|
|
7332
|
+
parent_id: remoteParentId
|
|
7333
|
+
};
|
|
7334
|
+
const existingRemoteFolder = await transports.getAssetFolder(remoteFolderId);
|
|
7335
|
+
const newRemoteFolder = existingRemoteFolder ? await transports.updateAssetFolder({ ...upsertFolder, parent_id: remoteParentId !== null ? remoteParentId : void 0 }) : await transports.createAssetFolder(upsertFolder);
|
|
7336
|
+
if (!maps.assetFolders.get(folder.id)) {
|
|
7337
|
+
await transports.appendAssetFolderManifest(folder, newRemoteFolder);
|
|
7338
|
+
}
|
|
7339
|
+
await transports.cleanupAssetFolder?.({ localFilePath: context.localFilePath });
|
|
7340
|
+
onFolderSuccess?.(folder, newRemoteFolder);
|
|
7341
|
+
} catch (maybeError) {
|
|
7342
|
+
onFolderError?.(toError(maybeError), folder);
|
|
7343
|
+
} finally {
|
|
7344
|
+
onIncrement?.();
|
|
7345
|
+
callback();
|
|
7346
|
+
}
|
|
7347
|
+
}
|
|
7348
|
+
});
|
|
7349
|
+
};
|
|
7350
|
+
const readLocalAssetsStream = ({
|
|
7351
|
+
directoryPath,
|
|
7352
|
+
setTotalAssets,
|
|
7353
|
+
onAssetError
|
|
7354
|
+
}) => {
|
|
7355
|
+
const iterator = async function* readAssets() {
|
|
7356
|
+
try {
|
|
7357
|
+
const files = await readdir(directoryPath);
|
|
7358
|
+
const metadataFiles = files.filter((file) => file.endsWith(".json") && file !== "manifest.jsonl");
|
|
7359
|
+
setTotalAssets?.(metadataFiles.length);
|
|
7360
|
+
for (const file of metadataFiles) {
|
|
7361
|
+
const filePath = join(directoryPath, file);
|
|
7362
|
+
try {
|
|
7363
|
+
const statResult = await stat(filePath);
|
|
7364
|
+
if (!statResult.isFile()) {
|
|
7365
|
+
continue;
|
|
7366
|
+
}
|
|
7367
|
+
const metadataContent = await readFile$1(filePath, "utf8");
|
|
7368
|
+
const assetRaw = JSON.parse(metadataContent);
|
|
7369
|
+
const asset = {
|
|
7370
|
+
...assetRaw,
|
|
7371
|
+
short_filename: assetRaw.short_filename || basename(assetRaw.filename)
|
|
7372
|
+
};
|
|
7373
|
+
const baseName = parse(file).name;
|
|
7374
|
+
const extFromMetadata = extname(asset.short_filename || asset.filename) || "";
|
|
7375
|
+
const assetBinaryPath = join(directoryPath, `${baseName}${extFromMetadata}`);
|
|
7376
|
+
const fileBuffer = await readFile$1(assetBinaryPath);
|
|
7377
|
+
yield {
|
|
7378
|
+
asset,
|
|
7379
|
+
context: {
|
|
7380
|
+
fileBuffer,
|
|
7381
|
+
assetBinaryPath,
|
|
7382
|
+
assetPath: filePath
|
|
7383
|
+
}
|
|
7384
|
+
};
|
|
7385
|
+
} catch (maybeError) {
|
|
7386
|
+
onAssetError?.(toError(maybeError));
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
} catch (maybeError) {
|
|
7390
|
+
onAssetError?.(toError(maybeError));
|
|
7391
|
+
}
|
|
7392
|
+
};
|
|
7393
|
+
return Readable.from(iterator());
|
|
7394
|
+
};
|
|
7395
|
+
const readSingleAssetStream = ({
|
|
7396
|
+
asset,
|
|
7397
|
+
assetBinaryPath,
|
|
7398
|
+
onAssetError
|
|
7399
|
+
}) => {
|
|
7400
|
+
const iterator = async function* readSingleAsset() {
|
|
7401
|
+
try {
|
|
7402
|
+
if (!isRemoteSource(assetBinaryPath) && !await fileExists(assetBinaryPath)) {
|
|
7403
|
+
throw new Error("Asset path must point to a file.");
|
|
7404
|
+
}
|
|
7405
|
+
const fileBuffer = isRemoteSource(assetBinaryPath) ? await downloadFile(assetBinaryPath) : await readFile$1(assetBinaryPath);
|
|
7406
|
+
yield {
|
|
7407
|
+
asset,
|
|
7408
|
+
context: {
|
|
7409
|
+
fileBuffer,
|
|
7410
|
+
assetBinaryPath
|
|
7411
|
+
}
|
|
7412
|
+
};
|
|
7413
|
+
} catch (maybeError) {
|
|
7414
|
+
onAssetError?.(toError(maybeError));
|
|
7415
|
+
}
|
|
7416
|
+
};
|
|
7417
|
+
return Readable.from(iterator());
|
|
7418
|
+
};
|
|
7419
|
+
const makeCreateAssetAPITransport = ({ spaceId }) => (asset, fileBuffer) => createAsset(asset, fileBuffer, { spaceId });
|
|
7420
|
+
const makeUpdateAssetAPITransport = ({
|
|
7421
|
+
spaceId
|
|
7422
|
+
}) => (asset, fileBuffer) => updateAsset(asset, fileBuffer, {
|
|
7423
|
+
spaceId
|
|
7424
|
+
});
|
|
7425
|
+
const makeAppendAssetManifestFSTransport = ({ manifestFile }) => async (localAsset, remoteAsset) => {
|
|
7426
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7427
|
+
await appendToFile(manifestFile, JSON.stringify({
|
|
7428
|
+
old_id: localAsset.id,
|
|
7429
|
+
new_id: remoteAsset.id,
|
|
7430
|
+
old_filename: localAsset.filename,
|
|
7431
|
+
new_filename: remoteAsset.filename,
|
|
7432
|
+
created_at: createdAt
|
|
7433
|
+
}));
|
|
7434
|
+
};
|
|
7435
|
+
const makeAppendAssetFolderManifestFSTransport = ({ manifestFile }) => async (localFolder, remoteFolder) => {
|
|
7436
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7437
|
+
await appendToFile(manifestFile, JSON.stringify({
|
|
7438
|
+
old_id: localFolder.id,
|
|
7439
|
+
new_id: remoteFolder.id,
|
|
7440
|
+
created_at: createdAt
|
|
7441
|
+
}));
|
|
7442
|
+
};
|
|
7443
|
+
const makeGetAssetAPITransport = ({ spaceId }) => async (assetId) => {
|
|
7444
|
+
const { data, response } = await mapiClient().assets.get({
|
|
7445
|
+
path: {
|
|
7446
|
+
space_id: spaceId,
|
|
7447
|
+
asset_id: assetId
|
|
7448
|
+
}
|
|
7449
|
+
});
|
|
7450
|
+
if (!response.ok && response.status !== 404) {
|
|
7451
|
+
handleAPIError("pull_asset", new FetchError(response.statusText, response));
|
|
7452
|
+
}
|
|
7453
|
+
if (data?.deleted_at) {
|
|
7454
|
+
return void 0;
|
|
7455
|
+
}
|
|
7456
|
+
return data;
|
|
7457
|
+
};
|
|
7458
|
+
const saveDelete = async (filePath) => {
|
|
7459
|
+
if (await fileExists(filePath)) {
|
|
7460
|
+
await unlink(filePath);
|
|
7461
|
+
}
|
|
7462
|
+
};
|
|
7463
|
+
const makeCleanupAssetFSTransport = () => async ({ assetBinaryPath, assetPath }) => {
|
|
7464
|
+
const assetOrSidecarPath = assetPath || getSidecarFilename(assetBinaryPath);
|
|
7465
|
+
await Promise.all([
|
|
7466
|
+
assetBinaryPath && saveDelete(assetBinaryPath),
|
|
7467
|
+
assetOrSidecarPath && saveDelete(assetOrSidecarPath)
|
|
7468
|
+
]);
|
|
7469
|
+
};
|
|
7470
|
+
const hasId = (a) => {
|
|
7471
|
+
return !!a && typeof a === "object" && "id" in a && typeof a.id === "number";
|
|
7472
|
+
};
|
|
7473
|
+
const hasShortFilename = (a) => {
|
|
7474
|
+
return !!a && typeof a === "object" && "short_filename" in a && typeof a.short_filename === "string";
|
|
7475
|
+
};
|
|
7476
|
+
const isDataUnchanged = (localAsset, remoteAsset) => {
|
|
7477
|
+
if (localAsset.asset_folder_id !== remoteAsset.asset_folder_id) {
|
|
7478
|
+
return false;
|
|
7479
|
+
}
|
|
7480
|
+
if (localAsset.alt !== remoteAsset.alt || localAsset.title !== remoteAsset.title || localAsset.copyright !== remoteAsset.copyright || localAsset.source !== remoteAsset.source || localAsset.is_private !== remoteAsset.is_private) {
|
|
7481
|
+
return false;
|
|
7482
|
+
}
|
|
7483
|
+
const localAssetMetadataEntries = Object.entries(localAsset.meta_data || {});
|
|
7484
|
+
const remoteAssetMetadataEntries = Object.entries(remoteAsset.meta_data || {});
|
|
7485
|
+
if (localAssetMetadataEntries.length !== remoteAssetMetadataEntries.length) {
|
|
7486
|
+
return false;
|
|
7487
|
+
}
|
|
7488
|
+
const hasChanges = localAssetMetadataEntries.some(([k, v]) => remoteAsset.meta_data && remoteAsset.meta_data[k] !== v);
|
|
7489
|
+
return !hasChanges;
|
|
7490
|
+
};
|
|
7491
|
+
const isAssetUnchanged = async (localAsset, remoteAsset, localFileBuffer, downloadAssetFileTransport) => {
|
|
7492
|
+
const remoteFileBuffer = await downloadAssetFileTransport(remoteAsset);
|
|
7493
|
+
const isFileUnchanged = sha256(localFileBuffer) === sha256(remoteFileBuffer);
|
|
7494
|
+
if (!isFileUnchanged) {
|
|
7495
|
+
return false;
|
|
7496
|
+
}
|
|
7497
|
+
return isDataUnchanged(localAsset, remoteAsset);
|
|
7498
|
+
};
|
|
7499
|
+
const makeDownloadAssetFileTransport = ({
|
|
7500
|
+
assetToken,
|
|
7501
|
+
region
|
|
7502
|
+
}) => (asset) => downloadAssetFile(asset, { assetToken, region });
|
|
7503
|
+
const processAsset = async ({
|
|
7504
|
+
localAsset,
|
|
7505
|
+
fileBuffer,
|
|
7506
|
+
assetBinaryPath,
|
|
7507
|
+
assetPath,
|
|
7508
|
+
transports,
|
|
7509
|
+
maps
|
|
7510
|
+
}) => {
|
|
7511
|
+
const remoteFolderId = localAsset.asset_folder_id && (maps.assetFolders.get(localAsset.asset_folder_id) || localAsset.asset_folder_id);
|
|
7512
|
+
const remoteAssetId = hasId(localAsset) ? maps.assets.get(localAsset.id)?.new.id || localAsset.id : void 0;
|
|
7513
|
+
const remoteAsset = remoteAssetId ? await transports.getAsset(remoteAssetId) : null;
|
|
7514
|
+
let newRemoteAsset;
|
|
7515
|
+
let status;
|
|
7516
|
+
if (remoteAsset) {
|
|
7517
|
+
const updatePayload = {
|
|
7518
|
+
...remoteAsset,
|
|
7519
|
+
...localAsset,
|
|
7520
|
+
id: remoteAsset.id,
|
|
7521
|
+
asset_folder_id: remoteFolderId
|
|
7522
|
+
};
|
|
7523
|
+
const canSkip = await isAssetUnchanged(
|
|
7524
|
+
updatePayload,
|
|
7525
|
+
remoteAsset,
|
|
7526
|
+
fileBuffer,
|
|
7527
|
+
transports.downloadAssetFile
|
|
7528
|
+
);
|
|
7529
|
+
if (canSkip) {
|
|
7530
|
+
newRemoteAsset = remoteAsset;
|
|
7531
|
+
status = "skipped";
|
|
7532
|
+
} else {
|
|
7533
|
+
newRemoteAsset = await transports.updateAsset(updatePayload, fileBuffer);
|
|
7534
|
+
status = "updated";
|
|
7535
|
+
}
|
|
7536
|
+
} else if (hasShortFilename(localAsset)) {
|
|
7537
|
+
const createPayload = {
|
|
7538
|
+
...localAsset,
|
|
7539
|
+
asset_folder_id: remoteFolderId
|
|
7540
|
+
};
|
|
7541
|
+
newRemoteAsset = await transports.createAsset(createPayload, fileBuffer);
|
|
7542
|
+
status = "created";
|
|
7543
|
+
} else {
|
|
7544
|
+
throw new Error("Could neither create nor update the asset: Missing ID and Filename");
|
|
7545
|
+
}
|
|
7546
|
+
if (hasId(localAsset)) {
|
|
7547
|
+
await transports.appendAssetManifest(localAsset, newRemoteAsset);
|
|
7548
|
+
}
|
|
7549
|
+
await transports.cleanupAsset?.({ assetBinaryPath, assetPath });
|
|
7550
|
+
return { status, remoteAsset: newRemoteAsset };
|
|
7551
|
+
};
|
|
7552
|
+
const upsertAssetStream = ({
|
|
7553
|
+
transports,
|
|
7554
|
+
maps,
|
|
7555
|
+
onIncrement,
|
|
7556
|
+
onAssetSuccess,
|
|
7557
|
+
onAssetSkipped,
|
|
7558
|
+
onAssetError
|
|
7559
|
+
}) => {
|
|
7560
|
+
const processing = /* @__PURE__ */ new Set();
|
|
7561
|
+
return new Writable({
|
|
7562
|
+
objectMode: true,
|
|
7563
|
+
async write({ asset: localAsset, context }, _encoding, callback) {
|
|
7564
|
+
await apiConcurrencyLock$1.acquire();
|
|
7565
|
+
const task = (async () => {
|
|
7566
|
+
try {
|
|
7567
|
+
const { status, remoteAsset } = await processAsset({
|
|
7568
|
+
localAsset,
|
|
7569
|
+
fileBuffer: context.fileBuffer,
|
|
7570
|
+
assetBinaryPath: context.assetBinaryPath,
|
|
7571
|
+
assetPath: context.assetPath,
|
|
7572
|
+
transports,
|
|
7573
|
+
maps
|
|
7574
|
+
});
|
|
7575
|
+
if (status === "skipped") {
|
|
7576
|
+
onAssetSkipped?.(localAsset, remoteAsset);
|
|
7577
|
+
} else {
|
|
7578
|
+
onAssetSuccess?.(localAsset, remoteAsset);
|
|
7579
|
+
}
|
|
7580
|
+
} catch (maybeError) {
|
|
7581
|
+
onAssetError?.(toError(maybeError), localAsset);
|
|
7582
|
+
}
|
|
7583
|
+
})();
|
|
7584
|
+
processing.add(task);
|
|
7585
|
+
task.finally(() => {
|
|
7586
|
+
onIncrement?.();
|
|
7587
|
+
apiConcurrencyLock$1.release();
|
|
7588
|
+
processing.delete(task);
|
|
7589
|
+
});
|
|
7590
|
+
callback();
|
|
7591
|
+
},
|
|
7592
|
+
final(callback) {
|
|
7593
|
+
Promise.all(processing).finally(() => callback());
|
|
7594
|
+
}
|
|
7595
|
+
});
|
|
7596
|
+
};
|
|
7597
|
+
|
|
7598
|
+
assetsCommand.command("pull").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter assets using Storyblok filter query syntax. Example: --query="search=my-file.jpg&with_tags=tag1,tag2"').option("--asset-token <token>", "Asset token for accessing private assets").description(`Download your space's assets as local files.`).action(async (options, command) => {
|
|
7599
|
+
const ui = getUI();
|
|
7600
|
+
const logger = getLogger();
|
|
7601
|
+
ui.title(`${commands.ASSETS}`, colorPalette.ASSETS, "Pulling assets...");
|
|
7602
|
+
logger.info("Pulling assets started");
|
|
7603
|
+
if (options.dryRun) {
|
|
7604
|
+
ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
|
|
7605
|
+
`);
|
|
7606
|
+
logger.warn("Dry run mode enabled");
|
|
7607
|
+
}
|
|
7608
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
7609
|
+
const assetToken = options.assetToken;
|
|
7610
|
+
const { state, initializeSession } = session();
|
|
7611
|
+
await initializeSession();
|
|
7612
|
+
if (!requireAuthentication(state, verbose)) {
|
|
7613
|
+
process.exitCode = 2;
|
|
7614
|
+
return;
|
|
7615
|
+
}
|
|
7616
|
+
if (!space) {
|
|
7617
|
+
handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
|
|
7618
|
+
process.exitCode = 2;
|
|
7619
|
+
return;
|
|
7620
|
+
}
|
|
7621
|
+
const { password, region } = state;
|
|
7622
|
+
mapiClient({
|
|
7623
|
+
token: {
|
|
7624
|
+
accessToken: password
|
|
7625
|
+
},
|
|
7626
|
+
region
|
|
7627
|
+
});
|
|
7628
|
+
const summary = {
|
|
7629
|
+
folderResults: { total: 0, succeeded: 0, failed: 0 },
|
|
7630
|
+
fetchAssetPages: { total: 0, succeeded: 0, failed: 0 },
|
|
7631
|
+
fetchAssets: { total: 0, succeeded: 0, failed: 0 },
|
|
7632
|
+
save: { total: 0, succeeded: 0, failed: 0 }
|
|
7633
|
+
};
|
|
7634
|
+
let fatalError = false;
|
|
7635
|
+
try {
|
|
7636
|
+
const folderProgress = ui.createProgressBar({ title: "Folders...".padEnd(25) });
|
|
7637
|
+
const fetchAssetPagesProgress = ui.createProgressBar({ title: "Fetching Asset Pages...".padEnd(24) });
|
|
7638
|
+
const fetchAssetsProgress = ui.createProgressBar({ title: "Fetching Assets...".padEnd(24) });
|
|
7639
|
+
const saveProgress = ui.createProgressBar({ title: "Saving Assets...".padEnd(24) });
|
|
7640
|
+
await pipeline$1(
|
|
7641
|
+
fetchAssetFoldersStream({
|
|
7642
|
+
spaceId: space,
|
|
7643
|
+
setTotalFolders: (total) => {
|
|
7644
|
+
summary.folderResults.total = total;
|
|
7645
|
+
folderProgress.setTotal(total);
|
|
7646
|
+
},
|
|
7647
|
+
onSuccess: () => {
|
|
7648
|
+
logger.info("Fetched asset folders");
|
|
7649
|
+
},
|
|
7650
|
+
onError: (error) => {
|
|
7651
|
+
summary.folderResults.failed += 1;
|
|
7652
|
+
summary.folderResults.total = summary.folderResults.total || 1;
|
|
7653
|
+
folderProgress.setTotal(summary.folderResults.total);
|
|
7654
|
+
logOnlyError(error);
|
|
7655
|
+
}
|
|
7656
|
+
}),
|
|
7657
|
+
writeAssetFolderStream({
|
|
7658
|
+
writeAssetFolder: options.dryRun ? async (folder) => folder : makeWriteAssetFolderFSTransport({
|
|
7659
|
+
directoryPath: resolveCommandPath(directories.assets, space, basePath)
|
|
7660
|
+
}),
|
|
7661
|
+
onIncrement: () => {
|
|
7662
|
+
folderProgress.increment();
|
|
7663
|
+
},
|
|
7664
|
+
onFolderSuccess: (folder) => {
|
|
7665
|
+
logger.info("Saved folder", { folderId: folder.id });
|
|
7666
|
+
summary.folderResults.succeeded += 1;
|
|
7667
|
+
},
|
|
7668
|
+
onFolderError: (error, folder) => {
|
|
7669
|
+
summary.folderResults.failed += 1;
|
|
7670
|
+
summary.folderResults.total = Math.max(summary.folderResults.total, summary.folderResults.succeeded + summary.folderResults.failed);
|
|
7671
|
+
logOnlyError(error, { folderId: folder.id });
|
|
7672
|
+
}
|
|
7673
|
+
})
|
|
7674
|
+
);
|
|
7675
|
+
await pipeline$1(
|
|
7676
|
+
fetchAssetsStream({
|
|
7677
|
+
spaceId: space,
|
|
7678
|
+
params: options.query ? Object.fromEntries(new URLSearchParams(options.query)) : {},
|
|
7679
|
+
setTotalAssets: (total) => {
|
|
7680
|
+
summary.fetchAssets.total = total;
|
|
7681
|
+
summary.save.total = total;
|
|
7682
|
+
fetchAssetsProgress.setTotal(total);
|
|
7683
|
+
saveProgress.setTotal(total);
|
|
7684
|
+
},
|
|
7685
|
+
setTotalPages: (totalPages) => {
|
|
7686
|
+
summary.fetchAssetPages.total = totalPages;
|
|
7687
|
+
fetchAssetPagesProgress.setTotal(totalPages);
|
|
7688
|
+
},
|
|
7689
|
+
onIncrement: () => {
|
|
7690
|
+
fetchAssetPagesProgress.increment();
|
|
7691
|
+
},
|
|
7692
|
+
onPageSuccess: (page, totalPages) => {
|
|
7693
|
+
logger.info(`Fetched assets page ${page} of ${totalPages}`);
|
|
7694
|
+
summary.fetchAssetPages.succeeded += 1;
|
|
7695
|
+
},
|
|
7696
|
+
onPageError: (error, page, totalPages) => {
|
|
7697
|
+
summary.fetchAssetPages.failed += 1;
|
|
7698
|
+
logOnlyError(error, { page, totalPages });
|
|
7699
|
+
}
|
|
7700
|
+
}),
|
|
7701
|
+
downloadAssetStream({
|
|
7702
|
+
assetToken,
|
|
7703
|
+
region,
|
|
7704
|
+
onIncrement: () => {
|
|
7705
|
+
fetchAssetsProgress.increment();
|
|
7706
|
+
},
|
|
7707
|
+
onAssetSuccess: (asset) => {
|
|
7708
|
+
logger.info("Fetched asset", { assetId: asset.id });
|
|
7709
|
+
summary.fetchAssets.succeeded += 1;
|
|
7710
|
+
},
|
|
7711
|
+
onAssetError: (error, asset) => {
|
|
7712
|
+
summary.fetchAssets.failed += 1;
|
|
7713
|
+
summary.save.total -= 1;
|
|
7714
|
+
saveProgress.setTotal(summary.save.total);
|
|
7715
|
+
logOnlyError(error, { assetId: asset.id });
|
|
7716
|
+
}
|
|
7717
|
+
}),
|
|
7718
|
+
writeAssetStream({
|
|
7719
|
+
writeAsset: options.dryRun ? async (asset) => asset : makeWriteAssetFSTransport({
|
|
7720
|
+
directoryPath: resolveCommandPath(directories.assets, space, basePath)
|
|
7721
|
+
}),
|
|
7722
|
+
onIncrement: () => {
|
|
7723
|
+
saveProgress.increment();
|
|
7724
|
+
},
|
|
7725
|
+
onAssetSuccess: (asset) => {
|
|
7726
|
+
logger.info("Saved asset", { assetId: asset.id });
|
|
7727
|
+
summary.save.succeeded += 1;
|
|
7728
|
+
},
|
|
7729
|
+
onAssetError: (error, asset) => {
|
|
7730
|
+
summary.save.failed += 1;
|
|
7731
|
+
logOnlyError(error, { assetId: asset.id });
|
|
7732
|
+
}
|
|
7733
|
+
})
|
|
7734
|
+
);
|
|
7735
|
+
} catch (maybeError) {
|
|
7736
|
+
fatalError = true;
|
|
7737
|
+
handleError(toError(maybeError));
|
|
7738
|
+
} finally {
|
|
7739
|
+
logger.info("Pulling assets finished", summary);
|
|
7740
|
+
ui.stopAllProgressBars();
|
|
7741
|
+
const failedAssets = Math.max(summary.fetchAssets.failed, summary.save.failed);
|
|
7742
|
+
const folderSummary = {
|
|
7743
|
+
total: summary.folderResults.total,
|
|
7744
|
+
succeeded: summary.folderResults.succeeded,
|
|
7745
|
+
failed: summary.folderResults.failed
|
|
7746
|
+
};
|
|
7747
|
+
ui.info(`Pull results: ${summary.save.total} assets pulled, ${failedAssets} assets failed`);
|
|
7748
|
+
ui.list([
|
|
7749
|
+
`Folders: ${folderSummary.succeeded}/${folderSummary.total} succeeded, ${folderSummary.failed} failed.`,
|
|
7750
|
+
`Fetching pages: ${summary.fetchAssetPages.succeeded}/${summary.fetchAssetPages.total} succeeded, ${summary.fetchAssetPages.failed} failed.`,
|
|
7751
|
+
`Fetching assets: ${summary.fetchAssets.succeeded}/${summary.fetchAssets.total} succeeded, ${summary.fetchAssets.failed} failed.`,
|
|
7752
|
+
`Saving assets: ${summary.save.succeeded}/${summary.save.total} succeeded, ${summary.save.failed} failed.`
|
|
7753
|
+
]);
|
|
7754
|
+
const reporter = getReporter();
|
|
7755
|
+
reporter.addSummary("folderResults", folderSummary);
|
|
7756
|
+
reporter.addSummary("fetchAssetPagesResults", summary.fetchAssetPages);
|
|
7757
|
+
reporter.addSummary("fetchAssetsResults", summary.fetchAssets);
|
|
7758
|
+
reporter.addSummary("saveResults", summary.save);
|
|
7759
|
+
reporter.finalize();
|
|
7760
|
+
const failedTotal = summary.folderResults.failed + summary.fetchAssetPages.failed + summary.fetchAssets.failed + summary.save.failed;
|
|
7761
|
+
process.exitCode = fatalError ? 2 : failedTotal > 0 ? 1 : 0;
|
|
7762
|
+
}
|
|
7763
|
+
});
|
|
7764
|
+
|
|
7765
|
+
const traverseAndMapBySchema = (data, {
|
|
7766
|
+
schemas,
|
|
7767
|
+
maps,
|
|
7768
|
+
fieldRefMappers: fieldRefMappers2,
|
|
7769
|
+
processedFields,
|
|
7770
|
+
missingSchemas
|
|
7771
|
+
}) => {
|
|
7772
|
+
const schema = schemas[data.component];
|
|
7773
|
+
if (!schema) {
|
|
7774
|
+
missingSchemas.add(data.component);
|
|
7775
|
+
return data;
|
|
7776
|
+
}
|
|
7777
|
+
const dataNew = { ...data };
|
|
7778
|
+
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
7779
|
+
const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
|
|
7780
|
+
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema && fieldSchema.type;
|
|
7781
|
+
const fieldRefMapper = typeof fieldType === "string" && fieldRefMappers2[fieldType];
|
|
7782
|
+
if (fieldSchema) {
|
|
7783
|
+
processedFields.add(fieldSchema);
|
|
7784
|
+
}
|
|
7785
|
+
if (fieldRefMapper) {
|
|
7786
|
+
dataNew[fieldName] = fieldRefMapper(fieldValue, {
|
|
7787
|
+
schema: fieldSchema,
|
|
7788
|
+
schemas,
|
|
7789
|
+
maps,
|
|
7790
|
+
fieldRefMappers: fieldRefMappers2,
|
|
7791
|
+
processedFields,
|
|
7792
|
+
missingSchemas
|
|
7793
|
+
});
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
return dataNew;
|
|
7797
|
+
};
|
|
7798
|
+
const traverseAndMapRichtextDoc = (data, {
|
|
7799
|
+
schemas,
|
|
7800
|
+
maps,
|
|
7801
|
+
fieldRefMappers: fieldRefMappers2,
|
|
7802
|
+
processedFields,
|
|
7803
|
+
missingSchemas
|
|
7804
|
+
}) => {
|
|
7805
|
+
if (Array.isArray(data)) {
|
|
7806
|
+
return data.map((item) => traverseAndMapRichtextDoc(item, {
|
|
7807
|
+
schemas,
|
|
7808
|
+
maps,
|
|
7809
|
+
fieldRefMappers: fieldRefMappers2,
|
|
7810
|
+
processedFields,
|
|
7811
|
+
missingSchemas
|
|
7812
|
+
}));
|
|
7813
|
+
}
|
|
7814
|
+
if (data && typeof data === "object") {
|
|
7815
|
+
if (data.type === "link" && data.attrs.linktype === "story") {
|
|
7816
|
+
return {
|
|
7817
|
+
...data,
|
|
7818
|
+
attrs: {
|
|
7819
|
+
...data.attrs,
|
|
7820
|
+
uuid: maps.stories?.get(data.attrs.uuid) || data.attrs.uuid
|
|
7821
|
+
}
|
|
7822
|
+
};
|
|
7823
|
+
}
|
|
7824
|
+
if (data.type === "blok") {
|
|
7825
|
+
return {
|
|
7826
|
+
...data,
|
|
7827
|
+
attrs: {
|
|
7828
|
+
...data.attrs,
|
|
7829
|
+
body: data.attrs.body.map((d) => traverseAndMapBySchema(d, {
|
|
7830
|
+
schemas,
|
|
7831
|
+
maps,
|
|
7832
|
+
fieldRefMappers: fieldRefMappers2,
|
|
7833
|
+
processedFields,
|
|
7834
|
+
missingSchemas
|
|
7835
|
+
}))
|
|
7836
|
+
}
|
|
7837
|
+
};
|
|
7838
|
+
}
|
|
7839
|
+
const newData = {};
|
|
7840
|
+
for (const [k, value] of Object.entries(data)) {
|
|
7841
|
+
newData[k] = traverseAndMapRichtextDoc(value, {
|
|
7842
|
+
schemas,
|
|
7843
|
+
maps,
|
|
7844
|
+
fieldRefMappers: fieldRefMappers2,
|
|
7845
|
+
processedFields,
|
|
7846
|
+
missingSchemas
|
|
7847
|
+
});
|
|
7848
|
+
}
|
|
7849
|
+
return newData;
|
|
7850
|
+
}
|
|
7851
|
+
return data;
|
|
7852
|
+
};
|
|
7853
|
+
const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => traverseAndMapRichtextDoc(data, {
|
|
7854
|
+
schemas,
|
|
7855
|
+
maps,
|
|
7856
|
+
fieldRefMappers: fieldRefMappers2,
|
|
7857
|
+
processedFields,
|
|
7858
|
+
missingSchemas
|
|
7859
|
+
});
|
|
7860
|
+
const multilinkFieldRefMapper = (data, { maps }) => {
|
|
7861
|
+
if (data.linktype !== "story") {
|
|
7862
|
+
return data;
|
|
7863
|
+
}
|
|
7864
|
+
return {
|
|
7865
|
+
...data,
|
|
7866
|
+
id: maps.stories?.get(data.id) || data.id
|
|
7867
|
+
};
|
|
7868
|
+
};
|
|
7869
|
+
const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => {
|
|
7870
|
+
if (!Array.isArray(data)) {
|
|
7871
|
+
throw new TypeError("Invalid data!");
|
|
7872
|
+
}
|
|
7873
|
+
return data.map((d) => traverseAndMapBySchema(d, {
|
|
7874
|
+
schemas,
|
|
7875
|
+
maps,
|
|
7876
|
+
fieldRefMappers: fieldRefMappers2,
|
|
7877
|
+
processedFields,
|
|
7878
|
+
missingSchemas
|
|
7879
|
+
}));
|
|
7880
|
+
};
|
|
7881
|
+
const assetFieldRefMapper = (data, { maps }) => {
|
|
7882
|
+
const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
|
|
7883
|
+
return {
|
|
7884
|
+
...data,
|
|
7885
|
+
...mappedAsset?.new
|
|
7886
|
+
};
|
|
7887
|
+
};
|
|
7888
|
+
const multiassetFieldRefMapper = (data, options) => {
|
|
7889
|
+
if (!Array.isArray(data)) {
|
|
7890
|
+
throw new TypeError("Invalid data!");
|
|
7891
|
+
}
|
|
7892
|
+
return data.map((d) => assetFieldRefMapper(d, options));
|
|
7893
|
+
};
|
|
7894
|
+
const optionsFieldRefMapper = (data, { schema, maps }) => {
|
|
7895
|
+
if (schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
7896
|
+
return data;
|
|
7897
|
+
}
|
|
7898
|
+
return data.map((d) => maps.stories?.get(d) || d);
|
|
7899
|
+
};
|
|
7900
|
+
const fieldRefMappers = {
|
|
7901
|
+
asset: assetFieldRefMapper,
|
|
7902
|
+
bloks: bloksFieldRefMapper,
|
|
7903
|
+
multiasset: multiassetFieldRefMapper,
|
|
7904
|
+
multilink: multilinkFieldRefMapper,
|
|
7905
|
+
options: optionsFieldRefMapper,
|
|
7906
|
+
richtext: richtextFieldRefMapper
|
|
7907
|
+
};
|
|
7908
|
+
const storyRefMapper = (story, { schemas, maps }) => {
|
|
7909
|
+
const processedFields = /* @__PURE__ */ new Set();
|
|
7910
|
+
const missingSchemas = /* @__PURE__ */ new Set();
|
|
7911
|
+
const alternates = story.alternates ? story.alternates.map((a) => ({
|
|
7912
|
+
...a,
|
|
7913
|
+
id: maps.stories?.get(a.id) || a.id,
|
|
7914
|
+
parent_id: maps.stories?.get(a.parent_id) || a.parent_id
|
|
7915
|
+
})) : story.alternates;
|
|
7916
|
+
const parentId = maps.stories?.get(story.parent_id) || story.parent_id;
|
|
7917
|
+
const mappedStory = {
|
|
7918
|
+
...story,
|
|
7919
|
+
content: traverseAndMapBySchema(story.content, {
|
|
7920
|
+
schemas,
|
|
7921
|
+
maps,
|
|
7922
|
+
fieldRefMappers,
|
|
7923
|
+
processedFields,
|
|
7924
|
+
missingSchemas
|
|
7925
|
+
}),
|
|
7926
|
+
id: Number(maps.stories?.get(story.id) || story.id),
|
|
7927
|
+
uuid: String(maps.stories?.get(story.uuid) || story.uuid),
|
|
7928
|
+
// @ts-expect-error Our types are wrong.
|
|
7929
|
+
parent_id: parentId ? Number(parentId) : null,
|
|
7930
|
+
alternates
|
|
7931
|
+
};
|
|
7932
|
+
return {
|
|
7933
|
+
mappedStory,
|
|
7934
|
+
processedFields,
|
|
7935
|
+
missingSchemas
|
|
7936
|
+
};
|
|
7937
|
+
};
|
|
7938
|
+
|
|
7939
|
+
const apiConcurrencyLock = new Sema(12);
|
|
7940
|
+
const fetchStoriesStream = ({
|
|
7941
|
+
spaceId,
|
|
7942
|
+
params = {},
|
|
7943
|
+
setTotalStories,
|
|
7944
|
+
setTotalPages,
|
|
7945
|
+
onIncrement,
|
|
7946
|
+
onPageSuccess,
|
|
7947
|
+
onPageError
|
|
7948
|
+
}) => {
|
|
7949
|
+
const listGenerator = async function* storyListIterator() {
|
|
7950
|
+
let perPage = 100;
|
|
7951
|
+
let page = 1;
|
|
7952
|
+
let totalPages = 1;
|
|
7953
|
+
setTotalPages?.(totalPages);
|
|
7954
|
+
while (page <= totalPages) {
|
|
7955
|
+
try {
|
|
7956
|
+
const result = await fetchStories(spaceId, {
|
|
7957
|
+
...params,
|
|
7958
|
+
per_page: perPage,
|
|
7959
|
+
page
|
|
7960
|
+
});
|
|
7961
|
+
if (!result) {
|
|
7962
|
+
break;
|
|
7963
|
+
}
|
|
7964
|
+
const { headers } = result;
|
|
7965
|
+
const total = Number(headers.get("Total"));
|
|
7966
|
+
perPage = Number(headers.get("Per-Page"));
|
|
7967
|
+
totalPages = Math.ceil(total / perPage);
|
|
7968
|
+
setTotalStories?.(total);
|
|
7969
|
+
setTotalPages?.(totalPages);
|
|
7970
|
+
onPageSuccess?.(page, totalPages);
|
|
7971
|
+
for (const story of result.stories) {
|
|
7972
|
+
yield story;
|
|
7973
|
+
}
|
|
7974
|
+
page += 1;
|
|
7975
|
+
} catch (maybeError) {
|
|
7976
|
+
onPageError?.(toError(maybeError), page, totalPages);
|
|
7977
|
+
break;
|
|
7978
|
+
} finally {
|
|
7979
|
+
onIncrement?.();
|
|
7980
|
+
}
|
|
7981
|
+
}
|
|
7982
|
+
};
|
|
7983
|
+
return Readable.from(listGenerator());
|
|
7984
|
+
};
|
|
7985
|
+
const fetchStoryStream = ({
|
|
7986
|
+
spaceId,
|
|
7987
|
+
onIncrement,
|
|
7988
|
+
onStorySuccess,
|
|
7989
|
+
onStoryError
|
|
7990
|
+
}) => {
|
|
7991
|
+
const processing = /* @__PURE__ */ new Set();
|
|
7992
|
+
return new Transform({
|
|
7993
|
+
objectMode: true,
|
|
7994
|
+
async transform(listStory, _encoding, callback) {
|
|
7995
|
+
await apiConcurrencyLock.acquire();
|
|
7996
|
+
const task = fetchStory(spaceId, listStory.id.toString()).then((story) => {
|
|
7997
|
+
if (typeof story === "undefined") {
|
|
7998
|
+
throw new TypeError("Invalid story!");
|
|
7999
|
+
}
|
|
8000
|
+
onStorySuccess?.(story);
|
|
8001
|
+
this.push(story);
|
|
8002
|
+
}).catch((maybeError) => {
|
|
8003
|
+
onStoryError?.(toError(maybeError), listStory);
|
|
8004
|
+
}).finally(() => {
|
|
8005
|
+
onIncrement?.();
|
|
8006
|
+
apiConcurrencyLock.release();
|
|
8007
|
+
processing.delete(task);
|
|
8008
|
+
});
|
|
8009
|
+
processing.add(task);
|
|
8010
|
+
callback();
|
|
8011
|
+
},
|
|
8012
|
+
// Ensure all pending requests finish before closing the stream
|
|
8013
|
+
flush(callback) {
|
|
8014
|
+
Promise.all(processing).finally(() => callback());
|
|
8015
|
+
}
|
|
8016
|
+
});
|
|
8017
|
+
};
|
|
8018
|
+
const getUUIDFromFilename = (filename) => {
|
|
8019
|
+
const uuid = basename(filename, extname(filename)).split("_").at(-1);
|
|
8020
|
+
if (!uuid) {
|
|
8021
|
+
throw new Error(`Unable to extract UUID from local story "${filename}"`);
|
|
8022
|
+
}
|
|
8023
|
+
return uuid;
|
|
8024
|
+
};
|
|
8025
|
+
const readLocalStoriesStream = ({
|
|
8026
|
+
directoryPath,
|
|
8027
|
+
fileFilter = () => true,
|
|
8028
|
+
setTotalStories,
|
|
8029
|
+
onIncrement,
|
|
8030
|
+
onStorySuccess,
|
|
8031
|
+
onStoryError
|
|
8032
|
+
}) => {
|
|
8033
|
+
const listGenerator = async function* localStoryIterator() {
|
|
8034
|
+
const files = (await readDirectory(directoryPath)).filter((f) => extname(f) === ".json" && fileFilter({ uuid: getUUIDFromFilename(f) }));
|
|
8035
|
+
setTotalStories?.(files.length);
|
|
8036
|
+
for (const file of files) {
|
|
8037
|
+
try {
|
|
8038
|
+
const filePath = join(directoryPath, file);
|
|
8039
|
+
const fileContent = await readFile$1(filePath, "utf-8");
|
|
8040
|
+
const story = JSON.parse(fileContent);
|
|
8041
|
+
onStorySuccess?.(story);
|
|
8042
|
+
yield story;
|
|
8043
|
+
} catch (maybeError) {
|
|
8044
|
+
onStoryError?.(toError(maybeError), file);
|
|
8045
|
+
} finally {
|
|
8046
|
+
onIncrement?.();
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
};
|
|
8050
|
+
return Readable.from(listGenerator());
|
|
8051
|
+
};
|
|
8052
|
+
const mapReferencesStream = ({
|
|
8053
|
+
schemas,
|
|
8054
|
+
maps,
|
|
8055
|
+
onIncrement,
|
|
8056
|
+
onStorySuccess,
|
|
8057
|
+
onStoryError
|
|
8058
|
+
}) => {
|
|
8059
|
+
return new Transform({
|
|
8060
|
+
objectMode: true,
|
|
8061
|
+
transform(localStory, _encoding, callback) {
|
|
8062
|
+
try {
|
|
8063
|
+
const { mappedStory, processedFields, missingSchemas } = storyRefMapper(localStory, { schemas, maps });
|
|
8064
|
+
onStorySuccess?.(mappedStory, processedFields, missingSchemas);
|
|
8065
|
+
this.push(mappedStory);
|
|
8066
|
+
} catch (maybeError) {
|
|
8067
|
+
onStoryError?.(toError(maybeError), localStory);
|
|
8068
|
+
} finally {
|
|
8069
|
+
onIncrement?.();
|
|
8070
|
+
callback();
|
|
8071
|
+
}
|
|
8072
|
+
}
|
|
8073
|
+
});
|
|
8074
|
+
};
|
|
8075
|
+
const getRemoteStory = async ({ spaceId, storyId }) => {
|
|
8076
|
+
const { data, response } = await mapiClient().stories.get({
|
|
8077
|
+
path: {
|
|
8078
|
+
space_id: spaceId,
|
|
8079
|
+
story_id: storyId
|
|
8080
|
+
}
|
|
8081
|
+
});
|
|
8082
|
+
if (!response.ok && response.status !== 404) {
|
|
8083
|
+
handleAPIError("pull_story", new FetchError(response.statusText, response));
|
|
8084
|
+
}
|
|
8085
|
+
if (data?.story?.deleted_at) {
|
|
8086
|
+
return void 0;
|
|
8087
|
+
}
|
|
8088
|
+
return data?.story;
|
|
8089
|
+
};
|
|
8090
|
+
const makeCreateStoryAPITransport = ({ spaceId }) => async (localStory) => {
|
|
8091
|
+
const { id: _id, uuid: _uuid, content, parent_id: _p, ...newStoryData } = localStory;
|
|
8092
|
+
const remoteStory = await createStory(spaceId, {
|
|
8093
|
+
story: {
|
|
8094
|
+
...newStoryData,
|
|
8095
|
+
content: {
|
|
8096
|
+
// @ts-expect-error Our types are wrong.
|
|
8097
|
+
component: content?.component
|
|
8098
|
+
}
|
|
8099
|
+
},
|
|
8100
|
+
publish: 0
|
|
8101
|
+
});
|
|
8102
|
+
if (!remoteStory) {
|
|
8103
|
+
throw new Error("No response!");
|
|
8104
|
+
}
|
|
8105
|
+
return remoteStory;
|
|
8106
|
+
};
|
|
8107
|
+
const makeAppendToManifestFSTransport = ({ manifestFile }) => async (localStory, remoteStory) => {
|
|
8108
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8109
|
+
await appendToFile(manifestFile, JSON.stringify({
|
|
8110
|
+
old_id: localStory.uuid,
|
|
8111
|
+
new_id: remoteStory.uuid,
|
|
8112
|
+
created_at: createdAt
|
|
8113
|
+
}));
|
|
8114
|
+
await appendToFile(manifestFile, JSON.stringify({
|
|
8115
|
+
old_id: localStory.id,
|
|
8116
|
+
new_id: remoteStory.id,
|
|
8117
|
+
created_at: createdAt
|
|
8118
|
+
}));
|
|
8119
|
+
};
|
|
8120
|
+
const createStoryPlaceholderStream = ({
|
|
8121
|
+
maps,
|
|
8122
|
+
spaceId,
|
|
8123
|
+
transports,
|
|
8124
|
+
onIncrement,
|
|
8125
|
+
onStorySuccess,
|
|
8126
|
+
onStorySkipped,
|
|
8127
|
+
onStoryError
|
|
8128
|
+
}) => {
|
|
8129
|
+
const processing = /* @__PURE__ */ new Set();
|
|
8130
|
+
return new Writable({
|
|
8131
|
+
objectMode: true,
|
|
8132
|
+
async write(localStory, _encoding, callback) {
|
|
8133
|
+
await apiConcurrencyLock.acquire();
|
|
8134
|
+
const task = (async () => {
|
|
8135
|
+
try {
|
|
8136
|
+
const mappedStoryId = maps.stories?.get(localStory.id);
|
|
8137
|
+
const mappedRemoteStory = mappedStoryId && await getRemoteStory({ spaceId, storyId: Number(mappedStoryId) });
|
|
8138
|
+
if (mappedRemoteStory) {
|
|
8139
|
+
onStorySkipped?.(localStory, mappedRemoteStory);
|
|
8140
|
+
return;
|
|
8141
|
+
}
|
|
8142
|
+
const existingRemoteStory = await getRemoteStory({ spaceId, storyId: localStory.id });
|
|
8143
|
+
if (existingRemoteStory && existingRemoteStory.uuid === localStory.uuid) {
|
|
8144
|
+
await transports.appendStoryManifest(localStory, existingRemoteStory);
|
|
8145
|
+
onStorySkipped?.(localStory, existingRemoteStory);
|
|
8146
|
+
return;
|
|
8147
|
+
}
|
|
8148
|
+
const newRemoteStory = await transports.createStory(localStory);
|
|
8149
|
+
await transports.appendStoryManifest(localStory, newRemoteStory);
|
|
8150
|
+
onStorySuccess?.(localStory, newRemoteStory);
|
|
8151
|
+
} catch (maybeError) {
|
|
8152
|
+
onStoryError?.(toError(maybeError), localStory);
|
|
8153
|
+
}
|
|
8154
|
+
})();
|
|
8155
|
+
processing.add(task);
|
|
8156
|
+
task.finally(() => {
|
|
8157
|
+
onIncrement?.();
|
|
8158
|
+
apiConcurrencyLock.release();
|
|
8159
|
+
processing.delete(task);
|
|
8160
|
+
});
|
|
8161
|
+
callback();
|
|
8162
|
+
},
|
|
8163
|
+
final(callback) {
|
|
8164
|
+
Promise.all(processing).finally(() => callback());
|
|
8165
|
+
}
|
|
8166
|
+
});
|
|
8167
|
+
};
|
|
8168
|
+
const makeWriteStoryFSTransport = ({ directoryPath }) => async (story) => {
|
|
8169
|
+
await saveToFile(resolve$1(directoryPath, getStoryFilename(story)), JSON.stringify(story, null, 2));
|
|
8170
|
+
return story;
|
|
8171
|
+
};
|
|
8172
|
+
const makeWriteStoryAPITransport = ({ spaceId, publish }) => (mappedLocalStory) => updateStory(spaceId, mappedLocalStory.id, {
|
|
8173
|
+
story: mappedLocalStory,
|
|
8174
|
+
publish: publish ?? (mappedLocalStory.published ? 1 : 0)
|
|
8175
|
+
});
|
|
8176
|
+
const makeCleanupStoryFSTransport = ({ directoryPath, maps }) => async (mappedStory) => {
|
|
8177
|
+
const mapEntry = maps.stories?.entries().find(([_, v]) => v === mappedStory.uuid);
|
|
8178
|
+
const originalUuid = mapEntry?.[0] && typeof mapEntry?.[0] === "string" ? mapEntry?.[0] : mappedStory.uuid;
|
|
8179
|
+
const storyFilename = getStoryFilename({
|
|
8180
|
+
slug: mappedStory.slug,
|
|
8181
|
+
uuid: originalUuid
|
|
8182
|
+
});
|
|
8183
|
+
const storyFilePath = resolve$1(directoryPath, storyFilename);
|
|
8184
|
+
await unlink(storyFilePath);
|
|
8185
|
+
};
|
|
8186
|
+
const writeStoryStream = ({
|
|
8187
|
+
transports,
|
|
8188
|
+
onIncrement,
|
|
8189
|
+
onStorySuccess,
|
|
8190
|
+
onStoryError
|
|
8191
|
+
}) => {
|
|
8192
|
+
const processing = /* @__PURE__ */ new Set();
|
|
8193
|
+
return new Writable({
|
|
8194
|
+
objectMode: true,
|
|
8195
|
+
async write(mappedLocalStory, _encoding, callback) {
|
|
8196
|
+
await apiConcurrencyLock.acquire();
|
|
8197
|
+
const task = (async () => {
|
|
8198
|
+
try {
|
|
8199
|
+
const remoteStory = await transports.writeStory(mappedLocalStory);
|
|
8200
|
+
await transports.cleanupStory?.(remoteStory);
|
|
8201
|
+
onStorySuccess?.(mappedLocalStory, remoteStory);
|
|
8202
|
+
} catch (maybeError) {
|
|
8203
|
+
onStoryError?.(toError(maybeError), mappedLocalStory);
|
|
8204
|
+
}
|
|
8205
|
+
})();
|
|
8206
|
+
processing.add(task);
|
|
8207
|
+
task.finally(() => {
|
|
8208
|
+
onIncrement?.();
|
|
8209
|
+
apiConcurrencyLock.release();
|
|
8210
|
+
processing.delete(task);
|
|
8211
|
+
});
|
|
8212
|
+
callback();
|
|
8213
|
+
},
|
|
8214
|
+
final(callback) {
|
|
8215
|
+
Promise.all(processing).finally(() => callback());
|
|
8216
|
+
}
|
|
8217
|
+
});
|
|
8218
|
+
};
|
|
8219
|
+
|
|
8220
|
+
const PROGRESS_BAR_PADDING = 23;
|
|
8221
|
+
const upsertAssetFoldersPipeline = async ({
|
|
8222
|
+
directoryPath,
|
|
8223
|
+
logger,
|
|
8224
|
+
maps,
|
|
8225
|
+
transports,
|
|
8226
|
+
ui
|
|
8227
|
+
}) => {
|
|
8228
|
+
const folderProgress = ui.createProgressBar({ title: "Folders...".padEnd(PROGRESS_BAR_PADDING) });
|
|
8229
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
8230
|
+
await pipeline$1(
|
|
8231
|
+
readLocalAssetFoldersStream({
|
|
8232
|
+
directoryPath,
|
|
8233
|
+
setTotalFolders: (total) => {
|
|
8234
|
+
summary.total = total;
|
|
8235
|
+
folderProgress.setTotal(total);
|
|
8236
|
+
},
|
|
8237
|
+
onFolderError: (error) => {
|
|
8238
|
+
summary.failed += 1;
|
|
8239
|
+
logOnlyError(error);
|
|
8240
|
+
}
|
|
8241
|
+
}),
|
|
8242
|
+
upsertAssetFolderStream({
|
|
8243
|
+
transports,
|
|
8244
|
+
maps,
|
|
8245
|
+
onIncrement: () => folderProgress.increment(),
|
|
8246
|
+
onFolderSuccess: (localFolder, remoteFolder) => {
|
|
8247
|
+
summary.succeeded += 1;
|
|
8248
|
+
maps.assetFolders.set(localFolder.id, remoteFolder.id);
|
|
8249
|
+
logger.info("Created asset folder", { folderId: remoteFolder.id });
|
|
8250
|
+
},
|
|
8251
|
+
onFolderError: (error, folder) => {
|
|
8252
|
+
summary.failed += 1;
|
|
8253
|
+
logOnlyError(error, { folderId: folder.id });
|
|
8254
|
+
}
|
|
8255
|
+
})
|
|
8256
|
+
);
|
|
8257
|
+
return [["assetFolderResults", summary]];
|
|
8258
|
+
};
|
|
8259
|
+
const upsertAssetsPipeline = async ({
|
|
8260
|
+
assetBinaryPath,
|
|
8261
|
+
assetData,
|
|
8262
|
+
directoryPath,
|
|
8263
|
+
logger,
|
|
8264
|
+
maps,
|
|
8265
|
+
transports,
|
|
8266
|
+
ui
|
|
8267
|
+
}) => {
|
|
8268
|
+
const assetProgress = ui.createProgressBar({ title: "Assets...".padEnd(PROGRESS_BAR_PADDING) });
|
|
8269
|
+
const summary = { total: 0, succeeded: 0, failed: 0, skipped: 0 };
|
|
8270
|
+
const steps = [];
|
|
8271
|
+
if (assetBinaryPath && assetData) {
|
|
8272
|
+
summary.total = 1;
|
|
8273
|
+
assetProgress.setTotal(1);
|
|
8274
|
+
steps.push(readSingleAssetStream({
|
|
8275
|
+
asset: assetData,
|
|
8276
|
+
assetBinaryPath,
|
|
8277
|
+
onAssetError: (error) => {
|
|
8278
|
+
summary.failed += 1;
|
|
8279
|
+
assetProgress.increment();
|
|
8280
|
+
logOnlyError(error);
|
|
8281
|
+
}
|
|
8282
|
+
}));
|
|
8283
|
+
} else {
|
|
8284
|
+
steps.push(readLocalAssetsStream({
|
|
8285
|
+
directoryPath,
|
|
8286
|
+
setTotalAssets: (total) => {
|
|
8287
|
+
summary.total = total;
|
|
8288
|
+
assetProgress.setTotal(total);
|
|
8289
|
+
},
|
|
8290
|
+
onAssetError: (error) => {
|
|
8291
|
+
summary.failed += 1;
|
|
8292
|
+
assetProgress.increment();
|
|
8293
|
+
logOnlyError(error);
|
|
8294
|
+
}
|
|
8295
|
+
}));
|
|
8296
|
+
}
|
|
8297
|
+
steps.push(upsertAssetStream({
|
|
8298
|
+
transports,
|
|
8299
|
+
maps,
|
|
8300
|
+
onIncrement: () => assetProgress.increment(),
|
|
8301
|
+
onAssetSuccess: (localAssetResult, remoteAsset) => {
|
|
8302
|
+
if ("id" in localAssetResult && localAssetResult.id) {
|
|
8303
|
+
maps.assets.set(localAssetResult.id, {
|
|
8304
|
+
old: localAssetResult,
|
|
8305
|
+
new: {
|
|
8306
|
+
id: remoteAsset.id,
|
|
8307
|
+
filename: remoteAsset.filename,
|
|
8308
|
+
meta_data: remoteAsset.meta_data
|
|
8309
|
+
}
|
|
8310
|
+
});
|
|
8311
|
+
}
|
|
8312
|
+
summary.succeeded += 1;
|
|
8313
|
+
logger.info("Uploaded asset", { assetId: remoteAsset.id });
|
|
8314
|
+
},
|
|
8315
|
+
onAssetSkipped: (localAssetResult, remoteAsset) => {
|
|
8316
|
+
if ("id" in localAssetResult && localAssetResult.id) {
|
|
8317
|
+
maps.assets.set(localAssetResult.id, {
|
|
8318
|
+
old: localAssetResult,
|
|
8319
|
+
new: {
|
|
8320
|
+
id: remoteAsset.id,
|
|
8321
|
+
filename: remoteAsset.filename,
|
|
8322
|
+
meta_data: remoteAsset.meta_data
|
|
8323
|
+
}
|
|
8324
|
+
});
|
|
8325
|
+
}
|
|
8326
|
+
summary.skipped += 1;
|
|
8327
|
+
logger.debug("Skipped asset (unchanged)", { assetId: remoteAsset.id });
|
|
8328
|
+
},
|
|
8329
|
+
onAssetError: (error, asset) => {
|
|
8330
|
+
summary.failed += 1;
|
|
8331
|
+
logOnlyError(error, { assetId: asset.id });
|
|
8332
|
+
}
|
|
8333
|
+
}));
|
|
8334
|
+
await pipeline$1(steps);
|
|
8335
|
+
return [["assetResults", summary]];
|
|
8336
|
+
};
|
|
8337
|
+
const mapAssetReferencesInStoriesPipeline = async ({
|
|
8338
|
+
logger,
|
|
8339
|
+
maps,
|
|
8340
|
+
schemas,
|
|
8341
|
+
space,
|
|
8342
|
+
transports,
|
|
8343
|
+
ui
|
|
8344
|
+
}) => {
|
|
8345
|
+
if (Object.keys(schemas).length === 0) {
|
|
8346
|
+
const message = "No components found. Please run `storyblok components pull` to fetch the latest components.";
|
|
8347
|
+
ui.error(message);
|
|
8348
|
+
logger.error(message);
|
|
8349
|
+
return [];
|
|
8350
|
+
}
|
|
8351
|
+
const fetchStoryPagesProgress = ui.createProgressBar({ title: "Fetching Story Pages...".padEnd(PROGRESS_BAR_PADDING) });
|
|
8352
|
+
const fetchStoriesProgress = ui.createProgressBar({ title: "Fetching Stories...".padEnd(PROGRESS_BAR_PADDING) });
|
|
8353
|
+
const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(PROGRESS_BAR_PADDING) });
|
|
8354
|
+
const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(PROGRESS_BAR_PADDING) });
|
|
8355
|
+
const summaries = {
|
|
8356
|
+
fetchStoryPages: { total: 0, succeeded: 0, failed: 0 },
|
|
8357
|
+
fetchStories: { total: 0, succeeded: 0, failed: 0 },
|
|
8358
|
+
storyProcessResults: { total: 0, succeeded: 0, failed: 0 },
|
|
8359
|
+
storyUpdateResults: { total: 0, succeeded: 0, failed: 0 }
|
|
8360
|
+
};
|
|
8361
|
+
const warnAboutMissingSchemas = (missingSchemas, story) => {
|
|
8362
|
+
const missingSchemaWarnings = /* @__PURE__ */ new Set();
|
|
8363
|
+
for (const schemaName of missingSchemas) {
|
|
8364
|
+
if (missingSchemaWarnings.has(schemaName)) {
|
|
8365
|
+
continue;
|
|
8366
|
+
}
|
|
8367
|
+
const message = `The component "${schemaName}" was not found. Please run \`storyblok components pull\` to fetch the latest components.`;
|
|
8368
|
+
logger.warn(message, { storyId: story.uuid });
|
|
8369
|
+
missingSchemaWarnings.add(schemaName);
|
|
8370
|
+
}
|
|
8371
|
+
};
|
|
8372
|
+
const assetMapValues = [...maps.assets.values()];
|
|
8373
|
+
const reference_search = assetMapValues.length === 1 ? assetMapValues[0].new.filename : void 0;
|
|
8374
|
+
await pipeline$1(
|
|
8375
|
+
fetchStoriesStream({
|
|
8376
|
+
spaceId: space,
|
|
8377
|
+
params: {
|
|
8378
|
+
reference_search
|
|
8379
|
+
},
|
|
8380
|
+
setTotalPages: (totalPages) => {
|
|
8381
|
+
summaries.fetchStoryPages.total = totalPages;
|
|
8382
|
+
fetchStoryPagesProgress.setTotal(totalPages);
|
|
8383
|
+
},
|
|
8384
|
+
setTotalStories: (total) => {
|
|
8385
|
+
summaries.fetchStories.total = total;
|
|
8386
|
+
summaries.storyProcessResults.total = total;
|
|
8387
|
+
summaries.storyUpdateResults.total = total;
|
|
8388
|
+
fetchStoriesProgress.setTotal(total);
|
|
8389
|
+
processProgress.setTotal(total);
|
|
8390
|
+
updateProgress.setTotal(total);
|
|
8391
|
+
},
|
|
8392
|
+
onIncrement: () => fetchStoryPagesProgress.increment(),
|
|
8393
|
+
onPageSuccess: (page, total) => {
|
|
8394
|
+
logger.info(`Fetched stories page ${page} of ${total}`);
|
|
8395
|
+
summaries.fetchStoryPages.succeeded += 1;
|
|
8396
|
+
},
|
|
8397
|
+
onPageError: (error, page, total) => {
|
|
8398
|
+
summaries.fetchStoryPages.failed += 1;
|
|
8399
|
+
logOnlyError(error, { page, total });
|
|
8400
|
+
}
|
|
8401
|
+
}),
|
|
8402
|
+
fetchStoryStream({
|
|
8403
|
+
spaceId: space,
|
|
8404
|
+
onIncrement: () => {
|
|
8405
|
+
fetchStoriesProgress.increment();
|
|
8406
|
+
},
|
|
8407
|
+
onStorySuccess: (story) => {
|
|
8408
|
+
logger.info("Fetched story", { storyId: story.id });
|
|
8409
|
+
summaries.fetchStories.succeeded += 1;
|
|
8410
|
+
},
|
|
8411
|
+
onStoryError: (error, story) => {
|
|
8412
|
+
summaries.fetchStories.failed += 1;
|
|
8413
|
+
summaries.storyProcessResults.total -= 1;
|
|
8414
|
+
summaries.storyUpdateResults.total -= 1;
|
|
8415
|
+
processProgress.setTotal(summaries.storyProcessResults.total);
|
|
8416
|
+
updateProgress.setTotal(summaries.storyProcessResults.total);
|
|
8417
|
+
logOnlyError(error, { storyId: story.id });
|
|
8418
|
+
}
|
|
8419
|
+
}),
|
|
8420
|
+
// Map all references to numeric ids and uuids.
|
|
8421
|
+
mapReferencesStream({
|
|
8422
|
+
schemas,
|
|
8423
|
+
maps: { stories: /* @__PURE__ */ new Map(), ...maps },
|
|
8424
|
+
onIncrement() {
|
|
8425
|
+
processProgress.increment();
|
|
8426
|
+
},
|
|
8427
|
+
onStorySuccess(localStory, _, missingSchemas) {
|
|
8428
|
+
warnAboutMissingSchemas(missingSchemas, localStory);
|
|
8429
|
+
logger.info("Processed story", { storyId: localStory.uuid });
|
|
8430
|
+
summaries.storyProcessResults.succeeded += 1;
|
|
8431
|
+
},
|
|
8432
|
+
onStoryError(error, localStory) {
|
|
8433
|
+
summaries.storyProcessResults.failed += 1;
|
|
8434
|
+
summaries.storyUpdateResults.total -= 1;
|
|
8435
|
+
updateProgress.setTotal(summaries.storyUpdateResults.total);
|
|
8436
|
+
logOnlyError(error, { storyId: localStory.id });
|
|
8437
|
+
}
|
|
8438
|
+
}),
|
|
8439
|
+
// Update remote stories with correct references.
|
|
8440
|
+
writeStoryStream({
|
|
8441
|
+
transports: {
|
|
8442
|
+
writeStory: transports.writeStory
|
|
8443
|
+
},
|
|
8444
|
+
onIncrement() {
|
|
8445
|
+
updateProgress.increment();
|
|
8446
|
+
},
|
|
8447
|
+
onStorySuccess(localStory) {
|
|
8448
|
+
logger.info("Updated story", { storyId: localStory.uuid });
|
|
8449
|
+
summaries.storyUpdateResults.succeeded += 1;
|
|
8450
|
+
},
|
|
8451
|
+
onStoryError(error, localStory) {
|
|
8452
|
+
summaries.storyUpdateResults.failed += 1;
|
|
8453
|
+
logOnlyError(error, { storyId: localStory.id });
|
|
8454
|
+
}
|
|
8455
|
+
})
|
|
8456
|
+
);
|
|
8457
|
+
return Object.entries(summaries);
|
|
8458
|
+
};
|
|
8459
|
+
|
|
8460
|
+
assetsCommand.command("push").argument("[asset]", "path or URL of a single asset to push").option("-f, --from <from>", "source space id").option("--data <data>", "inline asset data as JSON").option("--short-filename <short-filename>", "override the asset filename").option("--folder <folderId>", "destination asset folder ID").option("--cleanup", "delete local assets and metadata after a successful push (note: does not cleanup manifests)").option("--update-stories", "update file references in stories if necessary", false).option("--asset-token <token>", "asset token for accessing private assets").option("-d, --dry-run", "Preview changes without applying them to Storyblok").description(`Push local assets to a Storyblok space.`).action(async (assetInput, options, command) => {
|
|
8461
|
+
const ui = getUI();
|
|
8462
|
+
const logger = getLogger();
|
|
8463
|
+
const reporter = getReporter();
|
|
8464
|
+
ui.title(`${commands.ASSETS}`, colorPalette.ASSETS, "Pushing assets...");
|
|
8465
|
+
logger.info("Pushing assets started");
|
|
8466
|
+
if (options.dryRun) {
|
|
8467
|
+
ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
|
|
8468
|
+
`);
|
|
8469
|
+
logger.warn("Dry run mode enabled");
|
|
8470
|
+
}
|
|
8471
|
+
const { space: targetSpace, path: basePath, verbose } = command.optsWithGlobals();
|
|
8472
|
+
const fromSpace = options.from || targetSpace;
|
|
8473
|
+
const assetToken = options.assetToken;
|
|
8474
|
+
const { state, initializeSession } = session();
|
|
8475
|
+
await initializeSession();
|
|
8476
|
+
if (!requireAuthentication(state, verbose)) {
|
|
8477
|
+
process.exitCode = 2;
|
|
8478
|
+
return;
|
|
8479
|
+
}
|
|
8480
|
+
if (!targetSpace) {
|
|
8481
|
+
handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
|
|
8482
|
+
process.exitCode = 2;
|
|
8483
|
+
return;
|
|
8484
|
+
}
|
|
8485
|
+
const { password, region } = state;
|
|
8486
|
+
mapiClient({
|
|
8487
|
+
token: {
|
|
8488
|
+
accessToken: password
|
|
8489
|
+
},
|
|
8490
|
+
region
|
|
8491
|
+
});
|
|
8492
|
+
const summaries = [];
|
|
8493
|
+
let fatalError = false;
|
|
8494
|
+
const manifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "manifest.jsonl");
|
|
8495
|
+
const folderManifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "folders", "manifest.jsonl");
|
|
8496
|
+
try {
|
|
8497
|
+
const [assetMap, assetFolderMap] = await Promise.all([
|
|
8498
|
+
loadAssetMap(manifestFile),
|
|
8499
|
+
loadAssetFolderMap(folderManifestFile)
|
|
8500
|
+
]);
|
|
8501
|
+
const maps = { assets: assetMap, assetFolders: assetFolderMap };
|
|
8502
|
+
const assetsDirectoryPath = resolveCommandPath(directories.assets, fromSpace, basePath);
|
|
8503
|
+
const assetFolderGetTransport = makeGetAssetFolderAPITransport({ spaceId: targetSpace });
|
|
8504
|
+
const assetFolderCreateTransport = options.dryRun ? async (folder) => folder : makeCreateAssetFolderAPITransport({ spaceId: targetSpace });
|
|
8505
|
+
const assetFolderUpdateTransport = options.dryRun ? async (folder) => folder : makeUpdateAssetFolderAPITransport({ spaceId: targetSpace });
|
|
8506
|
+
const assetFolderManifestTransport = options.dryRun ? () => Promise.resolve() : makeAppendAssetFolderManifestFSTransport({ manifestFile: folderManifestFile });
|
|
8507
|
+
const cleanupAssetFolderTransport = options.cleanup && !options.dryRun ? makeCleanupAssetFolderFSTransport() : () => Promise.resolve();
|
|
8508
|
+
summaries.push(...await upsertAssetFoldersPipeline({
|
|
8509
|
+
directoryPath: join(assetsDirectoryPath, "folders"),
|
|
8510
|
+
logger,
|
|
8511
|
+
maps,
|
|
8512
|
+
transports: {
|
|
8513
|
+
getAssetFolder: assetFolderGetTransport,
|
|
8514
|
+
createAssetFolder: assetFolderCreateTransport,
|
|
8515
|
+
updateAssetFolder: assetFolderUpdateTransport,
|
|
8516
|
+
appendAssetFolderManifest: assetFolderManifestTransport,
|
|
8517
|
+
cleanupAssetFolder: cleanupAssetFolderTransport
|
|
8518
|
+
},
|
|
8519
|
+
ui
|
|
8520
|
+
}));
|
|
8521
|
+
const assetBinaryPath = typeof assetInput === "string" && assetInput.trim().length > 0 ? assetInput : void 0;
|
|
8522
|
+
let assetData;
|
|
8523
|
+
if (assetBinaryPath) {
|
|
8524
|
+
const assetDataPartial = options.data ? parseAssetData(options.data) : !isRemoteSource(assetBinaryPath) ? await loadSidecarAssetData(assetBinaryPath) : {};
|
|
8525
|
+
const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
|
|
8526
|
+
const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
|
|
8527
|
+
const folderId = options.folder ? Number(options.folder) : void 0;
|
|
8528
|
+
assetData = {
|
|
8529
|
+
...assetDataPartial,
|
|
8530
|
+
short_filename: shortFilename,
|
|
8531
|
+
asset_folder_id: folderId
|
|
8532
|
+
};
|
|
8533
|
+
}
|
|
8534
|
+
const getAssetTransport = makeGetAssetAPITransport({ spaceId: targetSpace });
|
|
8535
|
+
const createAssetTransport = options.dryRun ? async (asset) => asset : makeCreateAssetAPITransport({ spaceId: targetSpace });
|
|
8536
|
+
const updateAssetTransport = options.dryRun ? async (asset) => asset : makeUpdateAssetAPITransport({ spaceId: targetSpace });
|
|
8537
|
+
const downloadAssetFileTransport = makeDownloadAssetFileTransport({
|
|
8538
|
+
assetToken,
|
|
8539
|
+
region
|
|
8540
|
+
});
|
|
8541
|
+
const assetManifestTransport = options.dryRun ? () => Promise.resolve() : makeAppendAssetManifestFSTransport({ manifestFile });
|
|
8542
|
+
const cleanupAssetTransport = options.cleanup && !options.dryRun ? makeCleanupAssetFSTransport() : () => Promise.resolve();
|
|
8543
|
+
summaries.push(...await upsertAssetsPipeline({
|
|
8544
|
+
assetBinaryPath,
|
|
8545
|
+
assetData,
|
|
8546
|
+
directoryPath: assetsDirectoryPath,
|
|
8547
|
+
logger,
|
|
8548
|
+
maps,
|
|
8549
|
+
transports: {
|
|
8550
|
+
getAsset: getAssetTransport,
|
|
8551
|
+
createAsset: createAssetTransport,
|
|
8552
|
+
updateAsset: updateAssetTransport,
|
|
8553
|
+
downloadAssetFile: downloadAssetFileTransport,
|
|
8554
|
+
appendAssetManifest: assetManifestTransport,
|
|
8555
|
+
cleanupAsset: cleanupAssetTransport
|
|
8556
|
+
},
|
|
8557
|
+
ui
|
|
8558
|
+
}));
|
|
8559
|
+
const hasUpdatedFilename = (entry) => "filename" in entry.old && entry.old.filename !== entry.new.filename;
|
|
8560
|
+
const hasMetadata = (entry) => "meta_data" in entry.new && entry.new.meta_data;
|
|
8561
|
+
const hasUpdatedAssets = maps.assets.values().some((v) => hasUpdatedFilename(v) || hasMetadata(v));
|
|
8562
|
+
if (hasUpdatedAssets && options.updateStories) {
|
|
8563
|
+
const schemas = await findComponentSchemas(resolveCommandPath(directories.components, fromSpace, basePath));
|
|
8564
|
+
const writeStoryTransport = options.dryRun ? async (story) => story : makeWriteStoryAPITransport({ spaceId: targetSpace });
|
|
8565
|
+
summaries.push(...await mapAssetReferencesInStoriesPipeline({
|
|
8566
|
+
logger,
|
|
8567
|
+
maps,
|
|
8568
|
+
schemas,
|
|
8569
|
+
space: targetSpace,
|
|
8570
|
+
transports: {
|
|
8571
|
+
writeStory: writeStoryTransport
|
|
8572
|
+
},
|
|
8573
|
+
ui
|
|
8574
|
+
}));
|
|
8575
|
+
}
|
|
8576
|
+
if (!options.dryRun) {
|
|
8577
|
+
await deduplicateManifest(manifestFile);
|
|
8578
|
+
}
|
|
8579
|
+
} catch (maybeError) {
|
|
8580
|
+
fatalError = true;
|
|
8581
|
+
handleError(toError(maybeError), verbose);
|
|
8582
|
+
} finally {
|
|
8583
|
+
ui.stopAllProgressBars();
|
|
8584
|
+
const summary = Object.fromEntries(summaries);
|
|
8585
|
+
logger.info("Pushing assets finished", { summary });
|
|
8586
|
+
const assetsTotal = summary.assetResults?.total ?? 0;
|
|
8587
|
+
const assetsSucceeded = summary.assetResults?.succeeded ?? 0;
|
|
8588
|
+
const assetsSkipped = summary.assetResults?.skipped ?? 0;
|
|
8589
|
+
const assetsFailed = summary.assetResults?.failed ?? 0;
|
|
8590
|
+
ui.info(`Push results: ${assetsTotal} processed, ${assetsFailed} assets failed`);
|
|
8591
|
+
ui.list([
|
|
8592
|
+
`Folders: ${summary.assetFolderResults?.succeeded ?? 0}/${summary.assetFolderResults?.total ?? 0} succeeded, ${summary.assetFolderResults?.failed ?? 0} failed.`,
|
|
8593
|
+
`Assets: ${assetsSucceeded}/${assetsTotal} succeeded, ${assetsSkipped} skipped, ${assetsFailed} failed.`
|
|
8594
|
+
]);
|
|
8595
|
+
for (const [name, reportSummary] of summaries) {
|
|
8596
|
+
reporter.addSummary(name, reportSummary);
|
|
8597
|
+
}
|
|
8598
|
+
reporter.finalize();
|
|
8599
|
+
const failedTotal = Object.values(summary).reduce((total, entry) => {
|
|
8600
|
+
if (!entry || typeof entry.failed !== "number") {
|
|
8601
|
+
return total;
|
|
8602
|
+
}
|
|
8603
|
+
return total + entry.failed;
|
|
8604
|
+
}, 0);
|
|
8605
|
+
process.exitCode = fatalError ? 2 : failedTotal > 0 ? 1 : 0;
|
|
8606
|
+
}
|
|
8607
|
+
});
|
|
8608
|
+
|
|
8609
|
+
const program$1 = getProgram();
|
|
8610
|
+
const storiesCommand = program$1.command(commands.STORIES).description(`Manage your space's stories`).option("-s, --space <space>", "space ID");
|
|
8611
|
+
|
|
8612
|
+
storiesCommand.command("pull").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-p, --path <path>", "base path to store stories (default .storyblok)").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').description(`Download your space's stories as separate json files.`).action(async (options, command) => {
|
|
8613
|
+
const ui = getUI();
|
|
8614
|
+
const logger = getLogger();
|
|
8615
|
+
const reporter = getReporter();
|
|
8616
|
+
ui.title(`${commands.STORIES}`, colorPalette.STORIES, "Pulling stories...");
|
|
8617
|
+
logger.info("Pulling stories started");
|
|
8618
|
+
if (options.dryRun) {
|
|
8619
|
+
ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
|
|
8620
|
+
`);
|
|
8621
|
+
logger.warn("Dry run mode enabled");
|
|
8622
|
+
}
|
|
8623
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
8624
|
+
const { state, initializeSession } = session();
|
|
8625
|
+
await initializeSession();
|
|
8626
|
+
if (!requireAuthentication(state, verbose)) {
|
|
8627
|
+
return;
|
|
8628
|
+
}
|
|
8629
|
+
if (!space) {
|
|
8630
|
+
handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
|
|
8631
|
+
return;
|
|
8632
|
+
}
|
|
8633
|
+
const { password, region } = state;
|
|
8634
|
+
mapiClient({
|
|
8635
|
+
token: {
|
|
8636
|
+
accessToken: password
|
|
8637
|
+
},
|
|
8638
|
+
region
|
|
8639
|
+
});
|
|
8640
|
+
const summary = {
|
|
8641
|
+
fetchStoryPages: { total: 0, succeeded: 0, failed: 0 },
|
|
8642
|
+
fetchStories: { total: 0, succeeded: 0, failed: 0 },
|
|
8643
|
+
save: { total: 0, succeeded: 0, failed: 0 }
|
|
8644
|
+
};
|
|
8645
|
+
try {
|
|
8646
|
+
const fetchStoryPagesProgress = ui.createProgressBar({ title: "Fetching Story Pages...".padEnd(23) });
|
|
8647
|
+
const fetchStoriesProgress = ui.createProgressBar({ title: "Fetching Stories...".padEnd(23) });
|
|
8648
|
+
const saveProgress = ui.createProgressBar({ title: "Saving Stories...".padEnd(23) });
|
|
8649
|
+
await pipeline$1(
|
|
8650
|
+
fetchStoriesStream({
|
|
8651
|
+
spaceId: space,
|
|
8652
|
+
params: {
|
|
8653
|
+
filter_query: options.query,
|
|
8654
|
+
starts_with: options.startsWith
|
|
8655
|
+
},
|
|
8656
|
+
setTotalPages: (totalPages) => {
|
|
8657
|
+
summary.fetchStoryPages.total = totalPages;
|
|
8658
|
+
fetchStoryPagesProgress.setTotal(totalPages);
|
|
8659
|
+
},
|
|
8660
|
+
setTotalStories: (total) => {
|
|
8661
|
+
summary.fetchStories.total = total;
|
|
8662
|
+
summary.save.total = total;
|
|
8663
|
+
fetchStoriesProgress.setTotal(total);
|
|
8664
|
+
saveProgress.setTotal(total);
|
|
8665
|
+
},
|
|
8666
|
+
onIncrement: () => {
|
|
8667
|
+
fetchStoryPagesProgress.increment();
|
|
8668
|
+
},
|
|
8669
|
+
onPageSuccess: (page, total) => {
|
|
8670
|
+
logger.info(`Fetched stories page ${page} of ${total}`);
|
|
8671
|
+
summary.fetchStoryPages.succeeded += 1;
|
|
8672
|
+
},
|
|
8673
|
+
onPageError: (error, page, total) => {
|
|
8674
|
+
summary.fetchStoryPages.failed += 1;
|
|
8675
|
+
handleError(error, verbose, { page, total });
|
|
8676
|
+
}
|
|
8677
|
+
}),
|
|
8678
|
+
fetchStoryStream({
|
|
8679
|
+
spaceId: space,
|
|
8680
|
+
onIncrement: () => {
|
|
8681
|
+
fetchStoriesProgress.increment();
|
|
8682
|
+
},
|
|
8683
|
+
onStorySuccess: (story) => {
|
|
8684
|
+
logger.info("Fetched story", { storyId: story.id });
|
|
8685
|
+
summary.fetchStories.succeeded += 1;
|
|
8686
|
+
},
|
|
8687
|
+
onStoryError: (error, story) => {
|
|
8688
|
+
summary.fetchStories.failed += 1;
|
|
8689
|
+
summary.save.total -= 1;
|
|
8690
|
+
saveProgress.setTotal(summary.save.total);
|
|
8691
|
+
handleError(error, verbose, { storyId: story.id });
|
|
8692
|
+
}
|
|
8693
|
+
}),
|
|
8694
|
+
writeStoryStream({
|
|
8695
|
+
transports: {
|
|
8696
|
+
writeStory: options.dryRun ? async (story) => story : makeWriteStoryFSTransport({ directoryPath: resolveCommandPath(directories.stories, space, basePath) })
|
|
8697
|
+
},
|
|
8698
|
+
onIncrement: () => {
|
|
8699
|
+
saveProgress.increment();
|
|
8700
|
+
},
|
|
8701
|
+
onStorySuccess: (story) => {
|
|
8702
|
+
logger.info("Saved story", { storyId: story.id });
|
|
8703
|
+
summary.save.succeeded += 1;
|
|
8704
|
+
},
|
|
8705
|
+
onStoryError: (error, story) => {
|
|
8706
|
+
summary.save.failed += 1;
|
|
8707
|
+
handleError(error, verbose, { storyId: story.id });
|
|
8708
|
+
}
|
|
8709
|
+
})
|
|
8710
|
+
);
|
|
8711
|
+
} catch (maybeError) {
|
|
8712
|
+
handleError(toError(maybeError));
|
|
8713
|
+
} finally {
|
|
8714
|
+
logger.info("Pulling stories finished", summary);
|
|
8715
|
+
ui.stopAllProgressBars();
|
|
8716
|
+
ui.info(`Pull results: ${summary.save.total} stories pulled, ${Math.max(summary.fetchStories.failed, summary.save.failed)} stories failed`);
|
|
8717
|
+
ui.list([
|
|
8718
|
+
`Fetching pages: ${summary.fetchStoryPages.succeeded}/${summary.fetchStoryPages.total} succeeded, ${summary.fetchStoryPages.failed} failed.`,
|
|
8719
|
+
`Fetching stories: ${summary.fetchStories.succeeded}/${summary.fetchStories.total} succeeded, ${summary.fetchStories.failed} failed.`,
|
|
8720
|
+
`Saving stories: ${summary.save.succeeded}/${summary.save.total} succeeded, ${summary.save.failed} failed.`
|
|
8721
|
+
]);
|
|
8722
|
+
reporter.addSummary("fetchStoryPagesResults", summary.fetchStoryPages);
|
|
8723
|
+
reporter.addSummary("fetchStoriesResults", summary.fetchStories);
|
|
8724
|
+
reporter.addSummary("saveResults", summary.save);
|
|
8725
|
+
reporter.finalize();
|
|
8726
|
+
}
|
|
8727
|
+
});
|
|
8728
|
+
|
|
8729
|
+
storiesCommand.command("push").option("-f, --from <from>", "source space id").option("-p, --path <path>", "base path for stories and components (default .storyblok)").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("--publish", "Publish stories after pushing").option("--cleanup", "delete local stories after a successful push (note: does not cleanup manifests)").description(`Push local stories to a Storyblok space.`).action(async (options, command) => {
|
|
8730
|
+
const ui = getUI();
|
|
8731
|
+
const logger = getLogger();
|
|
8732
|
+
const reporter = getReporter();
|
|
8733
|
+
ui.title(`${commands.STORIES}`, colorPalette.STORIES, "Pushing stories...");
|
|
8734
|
+
logger.info("Pushing stories started");
|
|
8735
|
+
if (options.dryRun) {
|
|
8736
|
+
ui.warn(`DRY RUN MODE ENABLED: No changes will be made.
|
|
8737
|
+
`);
|
|
8738
|
+
logger.warn("Dry run mode enabled");
|
|
8739
|
+
}
|
|
8740
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
8741
|
+
const fromSpace = options.from || space;
|
|
8742
|
+
const { state, initializeSession } = session();
|
|
8743
|
+
await initializeSession();
|
|
8744
|
+
if (!requireAuthentication(state, verbose)) {
|
|
8745
|
+
return;
|
|
8746
|
+
}
|
|
8747
|
+
if (!space) {
|
|
8748
|
+
handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
|
|
8749
|
+
return;
|
|
8750
|
+
}
|
|
8751
|
+
const { password, region } = state;
|
|
8752
|
+
mapiClient({
|
|
8753
|
+
token: {
|
|
8754
|
+
accessToken: password
|
|
8755
|
+
},
|
|
8756
|
+
region
|
|
8757
|
+
});
|
|
8758
|
+
const warnAboutCustomPlugins = (fields, story) => {
|
|
8759
|
+
const warnedPlugins = /* @__PURE__ */ new Set();
|
|
8760
|
+
for (const field of fields) {
|
|
8761
|
+
if (field.type === "custom" && typeof field.field_type === "string") {
|
|
8762
|
+
if (warnedPlugins.has(field.field_type)) {
|
|
8763
|
+
continue;
|
|
8764
|
+
}
|
|
8765
|
+
warnedPlugins.add(field.field_type);
|
|
8766
|
+
const message = `The custom plugin "${field.field_type}" may contain references that require manual updates.`;
|
|
8767
|
+
ui.warn(message);
|
|
8768
|
+
logger.warn(message, { storyId: story.uuid });
|
|
8769
|
+
}
|
|
8770
|
+
}
|
|
8771
|
+
};
|
|
8772
|
+
const warnAboutMissingSchemas = (missingSchemas, story) => {
|
|
8773
|
+
const missingSchemaWarnings = /* @__PURE__ */ new Set();
|
|
8774
|
+
for (const schemaName of missingSchemas) {
|
|
8775
|
+
if (missingSchemaWarnings.has(schemaName)) {
|
|
8776
|
+
continue;
|
|
8777
|
+
}
|
|
8778
|
+
const message = `The component "${schemaName}" was not found. Please run \`storyblok components pull\` to fetch the latest components.`;
|
|
8779
|
+
ui.warn(message);
|
|
8780
|
+
logger.warn(message, { storyId: story.uuid });
|
|
8781
|
+
missingSchemaWarnings.add(schemaName);
|
|
8782
|
+
}
|
|
8783
|
+
};
|
|
8784
|
+
const summary = {
|
|
8785
|
+
creationResults: { total: 0, succeeded: 0, skipped: 0, failed: 0 },
|
|
8786
|
+
processResults: { total: 0, succeeded: 0, failed: 0 },
|
|
8787
|
+
updateResults: { total: 0, succeeded: 0, failed: 0 }
|
|
8788
|
+
};
|
|
8789
|
+
try {
|
|
8790
|
+
const manifestFile = join(resolveCommandPath(directories.stories, fromSpace, basePath), "manifest.jsonl");
|
|
8791
|
+
const manifest = await loadManifest(manifestFile);
|
|
8792
|
+
const assetManifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "manifest.jsonl");
|
|
8793
|
+
const maps = {
|
|
8794
|
+
assets: await loadAssetMap(assetManifestFile),
|
|
8795
|
+
stories: new Map(manifest.map((e) => [e.old_id, e.new_id]))
|
|
8796
|
+
};
|
|
8797
|
+
const schemas = await findComponentSchemas(resolveCommandPath(directories.components, fromSpace, basePath));
|
|
8798
|
+
if (Object.keys(schemas).length === 0) {
|
|
8799
|
+
const message = "No components found. Please run `storyblok components pull` to fetch the latest components.";
|
|
8800
|
+
ui.error(message);
|
|
8801
|
+
logger.error(message);
|
|
8802
|
+
return;
|
|
8803
|
+
}
|
|
8804
|
+
const storiesDirectoryPath = resolveCommandPath(directories.stories, fromSpace, basePath);
|
|
8805
|
+
const creationProgress = ui.createProgressBar({ title: "Creating Stories...".padEnd(21) });
|
|
8806
|
+
const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(21) });
|
|
8807
|
+
const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(21) });
|
|
8808
|
+
await pipeline$1(
|
|
8809
|
+
// Read local stories from `.json` files.
|
|
8810
|
+
readLocalStoriesStream({
|
|
8811
|
+
directoryPath: storiesDirectoryPath,
|
|
8812
|
+
setTotalStories(total) {
|
|
8813
|
+
summary.creationResults.total = total;
|
|
8814
|
+
summary.processResults.total = total;
|
|
8815
|
+
summary.updateResults.total = total;
|
|
8816
|
+
creationProgress.setTotal(total);
|
|
8817
|
+
processProgress.setTotal(total);
|
|
8818
|
+
updateProgress.setTotal(total);
|
|
8819
|
+
},
|
|
8820
|
+
onStoryError(error) {
|
|
8821
|
+
summary.creationResults.failed += 1;
|
|
8822
|
+
summary.processResults.total -= 1;
|
|
8823
|
+
summary.updateResults.total -= 1;
|
|
8824
|
+
processProgress.setTotal(summary.processResults.total);
|
|
8825
|
+
updateProgress.setTotal(summary.updateResults.total);
|
|
8826
|
+
creationProgress.increment();
|
|
8827
|
+
handleError(error, verbose);
|
|
8828
|
+
}
|
|
8829
|
+
}),
|
|
8830
|
+
// Create remote stories.
|
|
8831
|
+
createStoryPlaceholderStream({
|
|
8832
|
+
maps,
|
|
8833
|
+
spaceId: space,
|
|
8834
|
+
transports: {
|
|
8835
|
+
createStory: options.dryRun ? async (story) => story : makeCreateStoryAPITransport({
|
|
8836
|
+
maps,
|
|
8837
|
+
spaceId: space
|
|
8838
|
+
}),
|
|
8839
|
+
appendStoryManifest: options.dryRun ? () => Promise.resolve() : makeAppendToManifestFSTransport({
|
|
8840
|
+
manifestFile
|
|
8841
|
+
})
|
|
8842
|
+
},
|
|
8843
|
+
onStorySuccess(localStory, remoteStory) {
|
|
8844
|
+
if (!localStory.uuid || !remoteStory.uuid) {
|
|
8845
|
+
throw new Error("Invalid story provided!");
|
|
8846
|
+
}
|
|
8847
|
+
maps.stories.set(localStory.id, remoteStory.id);
|
|
8848
|
+
maps.stories.set(localStory.uuid, remoteStory.uuid);
|
|
8849
|
+
logger.info("Created story", { storyId: remoteStory.uuid });
|
|
8850
|
+
summary.creationResults.succeeded += 1;
|
|
8851
|
+
},
|
|
8852
|
+
onStorySkipped(localStory, remoteStory) {
|
|
8853
|
+
if (!localStory.uuid || !remoteStory.uuid) {
|
|
8854
|
+
throw new Error("Invalid story provided!");
|
|
8855
|
+
}
|
|
8856
|
+
maps.stories.set(localStory.id, remoteStory.id);
|
|
8857
|
+
maps.stories.set(localStory.uuid, remoteStory.uuid);
|
|
8858
|
+
logger.info("Skipped creating story", { storyId: localStory.uuid });
|
|
8859
|
+
summary.creationResults.skipped += 1;
|
|
8860
|
+
},
|
|
8861
|
+
onStoryError(error) {
|
|
8862
|
+
summary.creationResults.failed += 1;
|
|
8863
|
+
summary.processResults.total -= 1;
|
|
8864
|
+
summary.updateResults.total -= 1;
|
|
8865
|
+
processProgress.setTotal(summary.processResults.total);
|
|
8866
|
+
updateProgress.setTotal(summary.updateResults.total);
|
|
8867
|
+
handleError(error, verbose);
|
|
8868
|
+
},
|
|
8869
|
+
onIncrement() {
|
|
8870
|
+
creationProgress.increment();
|
|
8871
|
+
}
|
|
8872
|
+
})
|
|
8873
|
+
);
|
|
8874
|
+
await pipeline$1(
|
|
8875
|
+
// Read local stories from `.json` files.
|
|
8876
|
+
readLocalStoriesStream({
|
|
8877
|
+
directoryPath: storiesDirectoryPath,
|
|
8878
|
+
fileFilter({ uuid }) {
|
|
8879
|
+
return Boolean(maps.stories.get(uuid));
|
|
8880
|
+
},
|
|
8881
|
+
setTotalStories(total) {
|
|
8882
|
+
summary.processResults.total = total;
|
|
8883
|
+
summary.updateResults.total = total;
|
|
8884
|
+
processProgress.setTotal(total);
|
|
8885
|
+
updateProgress.setTotal(total);
|
|
8886
|
+
},
|
|
8887
|
+
onStoryError(error) {
|
|
8888
|
+
summary.creationResults.failed += 1;
|
|
8889
|
+
summary.processResults.total -= 1;
|
|
8890
|
+
summary.updateResults.total -= 1;
|
|
8891
|
+
processProgress.setTotal(summary.processResults.total);
|
|
8892
|
+
updateProgress.setTotal(summary.updateResults.total);
|
|
8893
|
+
handleError(error, verbose);
|
|
8894
|
+
}
|
|
8895
|
+
}),
|
|
8896
|
+
// Map all references to numeric ids and uuids.
|
|
8897
|
+
mapReferencesStream({
|
|
8898
|
+
schemas,
|
|
8899
|
+
maps,
|
|
8900
|
+
onIncrement() {
|
|
8901
|
+
processProgress.increment();
|
|
8902
|
+
},
|
|
8903
|
+
onStorySuccess(localStory, processedFields, missingSchemas) {
|
|
8904
|
+
warnAboutCustomPlugins(processedFields, localStory);
|
|
8905
|
+
warnAboutMissingSchemas(missingSchemas, localStory);
|
|
8906
|
+
logger.info("Processed story", { storyId: localStory.uuid });
|
|
8907
|
+
summary.processResults.succeeded += 1;
|
|
8908
|
+
},
|
|
8909
|
+
onStoryError(error, localStory) {
|
|
8910
|
+
summary.processResults.failed += 1;
|
|
8911
|
+
summary.updateResults.total -= 1;
|
|
8912
|
+
updateProgress.setTotal(summary.updateResults.total);
|
|
8913
|
+
handleError(error, verbose, { storyId: localStory.uuid });
|
|
8914
|
+
}
|
|
8915
|
+
}),
|
|
8916
|
+
// Update remote stories with correct references.
|
|
8917
|
+
writeStoryStream({
|
|
8918
|
+
transports: {
|
|
8919
|
+
writeStory: options.dryRun ? async (story) => story : makeWriteStoryAPITransport({
|
|
8920
|
+
spaceId: space,
|
|
8921
|
+
publish: options.publish ? 1 : void 0
|
|
8922
|
+
}),
|
|
8923
|
+
cleanupStory: options.cleanup && !options.dryRun ? makeCleanupStoryFSTransport({ directoryPath: storiesDirectoryPath, maps }) : void 0
|
|
8924
|
+
},
|
|
8925
|
+
onIncrement() {
|
|
8926
|
+
updateProgress.increment();
|
|
8927
|
+
},
|
|
8928
|
+
onStorySuccess(localStory) {
|
|
8929
|
+
logger.info("Updated story", { storyId: localStory.uuid });
|
|
8930
|
+
summary.updateResults.succeeded += 1;
|
|
8931
|
+
},
|
|
8932
|
+
onStoryError(error, localStory) {
|
|
8933
|
+
summary.updateResults.failed += 1;
|
|
8934
|
+
handleError(error, verbose, { storyId: localStory.uuid });
|
|
8935
|
+
}
|
|
8936
|
+
})
|
|
8937
|
+
);
|
|
8938
|
+
} catch (maybeError) {
|
|
8939
|
+
handleError(toError(maybeError));
|
|
8940
|
+
} finally {
|
|
8941
|
+
logger.info("Pushing stories finished", summary);
|
|
8942
|
+
ui.stopAllProgressBars();
|
|
8943
|
+
const failedStories = Math.max(summary.creationResults.failed, summary.processResults.failed, summary.updateResults.failed);
|
|
8944
|
+
ui.info(`Push results: ${summary.creationResults.total} ${summary.creationResults.total === 1 ? "story" : "stories"} pushed, ${failedStories} ${failedStories === 1 ? "story" : "stories"} failed`);
|
|
8945
|
+
ui.list([
|
|
8946
|
+
`Creating stories: ${summary.creationResults.succeeded + summary.creationResults.skipped}/${summary.creationResults.total} succeeded, ${summary.creationResults.failed} failed.`,
|
|
8947
|
+
`Processing stories: ${summary.processResults.succeeded}/${summary.processResults.total} succeeded, ${summary.processResults.failed} failed.`,
|
|
8948
|
+
`Updating stories: ${summary.updateResults.succeeded}/${summary.updateResults.total} succeeded, ${summary.updateResults.failed} failed.`
|
|
8949
|
+
]);
|
|
8950
|
+
reporter.addSummary("creationResults", summary.creationResults);
|
|
8951
|
+
reporter.addSummary("processResults", summary.processResults);
|
|
8952
|
+
reporter.addSummary("updateResults", summary.updateResults);
|
|
8953
|
+
reporter.finalize();
|
|
8954
|
+
}
|
|
8955
|
+
});
|
|
8956
|
+
|
|
6506
8957
|
const program = getProgram();
|
|
6507
8958
|
konsola.br();
|
|
6508
8959
|
konsola.br();
|