storyblok 4.20.0 → 4.21.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 CHANGED
@@ -714,6 +714,7 @@ const API_ACTIONS = {
714
714
  update_story: "Failed to update story",
715
715
  pull_asset: "Failed to pull asset",
716
716
  pull_assets: "Failed to pull assets",
717
+ transfer_enumerate_assets: "Failed to enumerate assets for transfer",
717
718
  pull_asset_folder: "Failed to pull asset folder",
718
719
  pull_asset_folders: "Failed to pull asset folders",
719
720
  push_asset_folder: "Failed to push asset folder",
@@ -7476,6 +7477,27 @@ const fetchAssets = async ({ spaceId, params }) => {
7476
7477
  handleAPIError("pull_assets", toError(maybeError));
7477
7478
  }
7478
7479
  };
7480
+ const fetchAllSpaceAssetIds = async (spaceId, params) => {
7481
+ try {
7482
+ const client = getMapiClient();
7483
+ const assets = await fetchAllPages(
7484
+ (page) => client.assets.list({
7485
+ path: { space_id: Number(spaceId) },
7486
+ query: { ...params, page, per_page: 100 },
7487
+ throwOnError: true
7488
+ }),
7489
+ (data) => data?.assets ?? []
7490
+ );
7491
+ return assets.reduce((ids, asset) => {
7492
+ if (typeof asset?.id === "number") {
7493
+ ids.push(asset.id);
7494
+ }
7495
+ return ids;
7496
+ }, []);
7497
+ } catch (maybeError) {
7498
+ handleAPIError("transfer_enumerate_assets", toError(maybeError));
7499
+ }
7500
+ };
7479
7501
  const fetchAssetInternalTagsByName = async (spaceId) => {
7480
7502
  try {
7481
7503
  const client = getMapiClient();
@@ -7651,22 +7673,32 @@ const transferAsset = async (spaceId, assetId, folderId) => {
7651
7673
  );
7652
7674
  }
7653
7675
  };
7676
+ const TRANSFER_CHUNK_SIZE = 500;
7654
7677
  const transferAssets = async (spaceId, assetIds, folderId, callbacks = {}) => {
7655
7678
  const lock = createPipelineBackpressureLock();
7656
- return Promise.all(assetIds.map(async (assetId) => {
7657
- await lock.acquire();
7658
- try {
7659
- const asset = await transferAsset(spaceId, assetId, folderId);
7660
- callbacks.onSuccess?.({ assetId, filename: asset.filename });
7661
- return { assetId, status: "transferred", filename: asset.filename };
7662
- } catch (maybeError) {
7663
- const error = toError(maybeError);
7664
- callbacks.onError?.(error, assetId);
7665
- return { assetId, status: "failed", reason: error.message };
7666
- } finally {
7667
- lock.release();
7668
- }
7669
- }));
7679
+ const results = [];
7680
+ let completed = 0;
7681
+ for (let start = 0; start < assetIds.length; start += TRANSFER_CHUNK_SIZE) {
7682
+ const chunk = assetIds.slice(start, start + TRANSFER_CHUNK_SIZE);
7683
+ const chunkResults = await Promise.all(chunk.map(async (assetId) => {
7684
+ await lock.acquire();
7685
+ try {
7686
+ const asset = await transferAsset(spaceId, assetId, folderId);
7687
+ callbacks.onSuccess?.({ assetId, filename: asset.filename });
7688
+ return { assetId, status: "transferred", filename: asset.filename };
7689
+ } catch (maybeError) {
7690
+ const error = toError(maybeError);
7691
+ callbacks.onError?.(error, assetId);
7692
+ return { assetId, status: "failed", reason: error.message };
7693
+ } finally {
7694
+ lock.release();
7695
+ completed += 1;
7696
+ callbacks.onProgress?.(completed, assetIds.length);
7697
+ }
7698
+ }));
7699
+ results.push(...chunkResults);
7700
+ }
7701
+ return results;
7670
7702
  };
7671
7703
  const fetchSharedAssets = async ({ spaceId, libraryId, params }) => {
7672
7704
  try {
@@ -10174,7 +10206,7 @@ pushCmd$1.action(async (assetInput, options, command) => {
10174
10206
  }
10175
10207
  });
10176
10208
 
10177
- const transferCmd = assetsCommand.command("transfer <asset-id...>").option("-s, --space <space>", "space ID").option("--folder-id <folderId>", "destination asset folder ID in the shared library").option("-d, --dry-run", "Preview changes without applying them to Storyblok").description(`Transfer space assets into the organization's shared asset library.`);
10209
+ const transferCmd = assetsCommand.command("transfer [asset-id...]").option("-s, --space <space>", "space ID").option("--folder-id <folderId>", "destination asset folder ID in the shared library").option("--all", "Transfer every asset in the space to the shared library").option("-q, --query <query>", 'Transfer every asset in the space matching a Storyblok filter query. Example: --query="search=my-file.jpg&with_tags=tag1,tag2"').option("-d, --dry-run", "Preview changes without applying them to Storyblok").description(`Transfer space assets into the organization's shared asset library.`);
10178
10210
  transferCmd.action(async (assetIds, options, command) => {
10179
10211
  const ui = getUI();
10180
10212
  const logger = getLogger();
@@ -10203,12 +10235,43 @@ transferCmd.action(async (assetIds, options, command) => {
10203
10235
  process.exitCode = 2;
10204
10236
  return;
10205
10237
  }
10206
- const ids = assetIds.map((id) => Number(id)).filter((id) => !Number.isNaN(id));
10207
- if (ids.length === 0) {
10208
- handleError(new CommandError(`Please provide at least one valid asset ID.`), verbose);
10238
+ if (options.all && options.query) {
10239
+ handleError(new CommandError(`Cannot combine --all with --query. Use --all to transfer every asset, or --query to transfer a filtered subset.`), verbose);
10240
+ process.exitCode = 2;
10241
+ return;
10242
+ }
10243
+ const bulk = Boolean(options.all || options.query);
10244
+ if (bulk && assetIds.length > 0) {
10245
+ handleError(new CommandError(`Cannot combine explicit asset IDs with --all or --query.`), verbose);
10209
10246
  process.exitCode = 2;
10210
10247
  return;
10211
10248
  }
10249
+ let ids;
10250
+ if (bulk) {
10251
+ const params = options.query ? Object.fromEntries(new URLSearchParams(options.query)) : void 0;
10252
+ logger.info("Enumerating space assets", { space, query: options.query });
10253
+ try {
10254
+ ids = await fetchAllSpaceAssetIds(space, params);
10255
+ } catch (maybeError) {
10256
+ handleError(toError(maybeError), verbose);
10257
+ process.exitCode = 2;
10258
+ return;
10259
+ }
10260
+ logger.info("Enumerated space assets", { count: ids.length });
10261
+ if (ids.length === 0) {
10262
+ ui.info(options.query ? `No assets in space ${space} match the query. Nothing to transfer.` : `No assets found in space ${space}. Nothing to transfer.`);
10263
+ logger.info("Transferring assets finished (no assets found)");
10264
+ process.exitCode = 0;
10265
+ return;
10266
+ }
10267
+ } else {
10268
+ ids = assetIds.map((id) => Number(id)).filter((id) => !Number.isNaN(id));
10269
+ if (ids.length === 0) {
10270
+ handleError(new CommandError(`Please provide at least one valid asset ID, or use --all.`), verbose);
10271
+ process.exitCode = 2;
10272
+ return;
10273
+ }
10274
+ }
10212
10275
  if (options.dryRun) {
10213
10276
  const summary2 = { total: ids.length, succeeded: 0, failed: 0 };
10214
10277
  ui.info(`Transfer plan: ${ids.length} asset(s) to folder ${folderId}`);
@@ -10219,14 +10282,33 @@ transferCmd.action(async (assetIds, options, command) => {
10219
10282
  process.exitCode = 0;
10220
10283
  return;
10221
10284
  }
10285
+ const progress = ui.createProgressBar({ title: "Transferring assets..." });
10286
+ progress.setTotal(ids.length);
10222
10287
  const results = await transferAssets(space, ids, folderId, {
10223
10288
  onSuccess: ({ assetId, filename }) => logger.info("Transferred asset", { assetId, filename }),
10224
- onError: (error, assetId) => logOnlyError(error, { assetId })
10289
+ onError: (error, assetId) => logOnlyError(error, { assetId }),
10290
+ onProgress: () => progress.increment()
10225
10291
  });
10292
+ ui.stopAllProgressBars();
10226
10293
  const succeeded = results.filter((result) => result.status === "transferred").length;
10227
10294
  const summary = { total: results.length, succeeded, failed: results.length - succeeded };
10228
- ui.info(`Transfer results: ${summary.total} processed, ${summary.failed} failed`);
10229
- ui.list(results.map((result) => result.status === "transferred" ? `${result.assetId} \u2713 transferred -> ${result.filename}` : `${result.assetId} \u2717 failed: ${result.reason}`));
10295
+ ui.info(`Transfer results: ${summary.succeeded} transferred, ${summary.failed} failed (of ${summary.total}).`);
10296
+ if (summary.failed > 0) {
10297
+ const countsByReason = /* @__PURE__ */ new Map();
10298
+ for (const result of results) {
10299
+ if (result.status === "failed") {
10300
+ const reason = result.reason ?? "Unknown error";
10301
+ countsByReason.set(reason, (countsByReason.get(reason) ?? 0) + 1);
10302
+ }
10303
+ }
10304
+ const byReason = [...countsByReason.entries()].sort((a, b) => b[1] - a[1]);
10305
+ const MAX_REASONS = 10;
10306
+ const lines = byReason.slice(0, MAX_REASONS).map(([reason, count]) => `\u2717 ${reason} (${count})`);
10307
+ if (byReason.length > MAX_REASONS) {
10308
+ lines.push(`\u2026 and ${byReason.length - MAX_REASONS} more failure reason(s)`);
10309
+ }
10310
+ ui.list(lines);
10311
+ }
10230
10312
  reporter.addSummary("transferResults", summary);
10231
10313
  reporter.finalize();
10232
10314
  logger.info("Transferring assets finished", { summary });