storyblok 4.4.1 → 4.6.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
@@ -7,6 +7,8 @@ import { Command } from 'commander';
7
7
  import { readPackageUp } from 'read-package-up';
8
8
  import { Spinner } from '@topcli/spinner';
9
9
  import { select, password, input, confirm } from '@inquirer/prompts';
10
+ import { ManagementApiClient } from '@storyblok/management-api-client';
11
+ import { RateLimit, Sema } from 'async-sema';
10
12
  import fs, { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
11
13
  import path, { join, parse, resolve } from 'node:path';
12
14
  import filenamify from 'filenamify';
@@ -14,7 +16,9 @@ import { exec, spawn } from 'node:child_process';
14
16
  import { promisify } from 'node:util';
15
17
  import { getRegion } from '@storyblok/region-helper';
16
18
  import { minimatch } from 'minimatch';
19
+ import { Readable, pipeline, Transform, Writable } from 'node:stream';
17
20
  import { hash } from 'ohash';
21
+ import { MultiBar, Presets } from 'cli-progress';
18
22
  import { compile } from 'json-schema-to-typescript';
19
23
  import { readFileSync } from 'node:fs';
20
24
  import open from 'open';
@@ -187,6 +191,7 @@ const API_ACTIONS = {
187
191
  update_datasource: "Failed to update datasource",
188
192
  delete_datasource: "Failed to delete datasource",
189
193
  create_space: "Failed to create space",
194
+ pull_spaces: "Failed to pull spaces",
190
195
  fetch_blueprints: "Failed to fetch blueprints from GitHub"
191
196
  };
192
197
  const API_ERRORS = {
@@ -407,12 +412,13 @@ function requireAuthentication(state, verbose = false) {
407
412
  return true;
408
413
  }
409
414
 
410
- const toPascalCase = (str) => {
411
- return str.replace(/(?:^|_)(\w)/g, (_, char) => char.toUpperCase());
412
- };
413
415
  const toCamelCase = (str) => {
414
416
  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/_/g, "").replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]([a-z])/gi, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]/gi, "");
415
417
  };
418
+ const toPascalCase = (str) => {
419
+ const camelCase = toCamelCase(str);
420
+ return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase;
421
+ };
416
422
  const capitalize = (str) => {
417
423
  return str.charAt(0).toUpperCase() + str.slice(1);
418
424
  };
@@ -427,19 +433,6 @@ function maskToken(token) {
427
433
  const maskedPart = "*".repeat(token.length - 4);
428
434
  return `${visiblePart}${maskedPart}`;
429
435
  }
430
- const objectToStringParams = (obj) => {
431
- return Object.entries(obj).reduce((acc, [key, value]) => {
432
- if (value === void 0) {
433
- return acc;
434
- }
435
- if (typeof value === "object" && value !== null) {
436
- acc[key] = JSON.stringify(value);
437
- } else {
438
- acc[key] = String(value);
439
- }
440
- return acc;
441
- }, {});
442
- };
443
436
  function createRegexFromGlob(pattern) {
444
437
  return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*")}$`);
445
438
  }
@@ -542,15 +535,86 @@ const getStoryblokUrl = (region = "eu") => {
542
535
  return `https://${managementApiRegions[region]}/${API_VERSION}`;
543
536
  };
544
537
 
545
- const loginWithToken = async (token, region) => {
538
+ let instance = null;
539
+ let storedConfig = null;
540
+ const lim = RateLimit(6, {
541
+ uniformDistribution: true
542
+ });
543
+ function configsAreEqual(config1, config2) {
544
+ return JSON.stringify(config1) === JSON.stringify(config2);
545
+ }
546
+ function mapiClient(options) {
547
+ if (!instance && options) {
548
+ instance = new ManagementApiClient(options);
549
+ instance.interceptors.request.use(async (request) => {
550
+ await lim();
551
+ return request;
552
+ });
553
+ storedConfig = options;
554
+ } else if (!instance) {
555
+ throw new Error("MAPI client not initialized. Call mapiClient with configuration first.");
556
+ } else if (options && storedConfig && !configsAreEqual(options, storedConfig)) {
557
+ instance = new ManagementApiClient(options);
558
+ instance.interceptors.request.use(async (request) => {
559
+ await lim();
560
+ return request;
561
+ });
562
+ storedConfig = options;
563
+ }
564
+ return instance;
565
+ }
566
+
567
+ const getUser = async (token, region) => {
546
568
  try {
547
- const url = getStoryblokUrl(region);
548
- return await customFetch(`${url}/users/me`, {
549
- headers: {
550
- Authorization: token
551
- }
569
+ const client = mapiClient({
570
+ token: {
571
+ accessToken: token
572
+ },
573
+ region
574
+ });
575
+ const { data } = await client.users.me({
576
+ throwOnError: true
552
577
  });
578
+ return data?.user;
579
+ } catch (error) {
580
+ if (error instanceof FetchError) {
581
+ const status = error.response.status;
582
+ switch (status) {
583
+ case 401:
584
+ throw new APIError("unauthorized", "get_user", error, `The token provided ${chalk.bold(maskToken(token))} is invalid.
585
+ Please make sure you are using the correct token and try again.`);
586
+ default:
587
+ throw new APIError("network_error", "get_user", error);
588
+ }
589
+ }
590
+ if (typeof error === "string" && error === "Unauthorized") {
591
+ const mockFetchError = new FetchError("Non-JSON response", {
592
+ status: 401,
593
+ statusText: "Unauthorized",
594
+ data: null
595
+ });
596
+ throw new APIError("unauthorized", "get_user", mockFetchError, `The token provided ${chalk.bold(maskToken(token))} is invalid.
597
+ Please make sure you are using the correct token and try again.`);
598
+ }
599
+ if (typeof error === "object" && error !== null && Object.keys(error).length === 0) {
600
+ const mockFetchError = new FetchError("Network Error", {
601
+ status: 500,
602
+ statusText: "Internal Server Error",
603
+ data: null
604
+ });
605
+ throw new APIError("network_error", "get_user", mockFetchError);
606
+ }
607
+ throw new APIError("generic", "get_user", error);
608
+ }
609
+ };
610
+
611
+ const loginWithToken = async (token, region) => {
612
+ try {
613
+ return await getUser(token, region);
553
614
  } catch (error) {
615
+ if (error instanceof APIError) {
616
+ throw error;
617
+ }
554
618
  if (error instanceof FetchError) {
555
619
  const status = error.response.status;
556
620
  switch (status) {
@@ -817,11 +881,13 @@ program$i.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
817
881
  });
818
882
  }
819
883
  spinner.start(`Logging in with token`);
820
- const { user } = await loginWithToken(token, userRegion);
821
- updateSession(user.email, token, userRegion);
822
- await persistCredentials(userRegion);
823
- spinner.succeed();
824
- konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(user.friendly_name)}.`, true);
884
+ const user = await loginWithToken(token, userRegion);
885
+ if (user) {
886
+ updateSession(user.email, token, userRegion);
887
+ await persistCredentials(userRegion);
888
+ spinner.succeed();
889
+ konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(user.friendly_name)}.`, true);
890
+ }
825
891
  } catch (error) {
826
892
  spinner.failed();
827
893
  konsola.br();
@@ -852,11 +918,13 @@ program$i.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
852
918
  });
853
919
  }
854
920
  spinner.start(`Logging in with token`);
855
- const { user } = await loginWithToken(userToken, userRegion);
921
+ const user = await loginWithToken(userToken, userRegion);
856
922
  spinner.succeed();
857
- updateSession(user.email, userToken, userRegion);
858
- await persistCredentials(userRegion);
859
- konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(user.friendly_name)}.`, true);
923
+ if (user) {
924
+ updateSession(user.email, userToken, userRegion);
925
+ await persistCredentials(userRegion);
926
+ konsola.ok(`Successfully logged in to region ${chalk.hex(colorPalette.PRIMARY)(`${regionNames[userRegion]} (${userRegion})`)}. Welcome ${chalk.hex(colorPalette.PRIMARY)(user.friendly_name)}.`, true);
927
+ }
860
928
  } else {
861
929
  const userEmail = await input({
862
930
  message: "Please enter your email address:",
@@ -983,30 +1051,6 @@ program$g.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
983
1051
  konsola.br();
984
1052
  });
985
1053
 
986
- const getUser = async (token, region) => {
987
- try {
988
- const url = getStoryblokUrl(region);
989
- const response = await customFetch(`${url}/users/me`, {
990
- headers: {
991
- Authorization: token
992
- }
993
- });
994
- return response;
995
- } catch (error) {
996
- if (error instanceof FetchError) {
997
- const status = error.response.status;
998
- switch (status) {
999
- case 401:
1000
- throw new APIError("unauthorized", "get_user", error, `The token provided ${chalk.bold(maskToken(token))} is invalid.
1001
- Please make sure you are using the correct token and try again.`);
1002
- default:
1003
- throw new APIError("network_error", "get_user", error);
1004
- }
1005
- }
1006
- throw new APIError("generic", "get_user", error);
1007
- }
1008
- };
1009
-
1010
1054
  const program$f = getProgram();
1011
1055
  program$f.command(commands.USER).description("Get the current user").action(async () => {
1012
1056
  konsola.title(`${commands.USER}`, colorPalette.USER);
@@ -1024,12 +1068,14 @@ program$f.command(commands.USER).description("Get the current user").action(asyn
1024
1068
  if (!password || !region) {
1025
1069
  throw new Error("No password or region found");
1026
1070
  }
1027
- const { user } = await getUser(password, region);
1028
- spinner.succeed();
1029
- if (verbose) {
1030
- konsola.info(JSON.stringify(user, null, 2));
1071
+ const user = await getUser(password, region);
1072
+ if (user) {
1073
+ if (verbose) {
1074
+ konsola.info(JSON.stringify(user, null, 2));
1075
+ }
1076
+ spinner.succeed();
1077
+ konsola.ok(`Hi ${chalk.bold(user.friendly_name)}, you are currently logged in with ${chalk.hex(colorPalette.PRIMARY)(user.email)} on ${chalk.bold(region)} region`, true);
1031
1078
  }
1032
- konsola.ok(`Hi ${chalk.bold(user.friendly_name)}, you are currently logged in with ${chalk.hex(colorPalette.PRIMARY)(user.email)} on ${chalk.bold(region)} region`, true);
1033
1079
  } catch (error) {
1034
1080
  spinner.failed();
1035
1081
  handleError(error, true);
@@ -1062,196 +1108,72 @@ const resolveRegion = async (thisCommand) => {
1062
1108
  const program$e = getProgram();
1063
1109
  const componentsCommand = program$e.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").hook("preAction", resolveRegion);
1064
1110
 
1065
- let instance = null;
1066
- const createMapiClient = (options) => {
1067
- const baseHeaders = {
1068
- "Content-Type": "application/json",
1069
- "Authorization": options.token
1070
- };
1071
- const state = {
1072
- uuid: `mapi-client-${Math.random().toString(36).substring(2, 15)}`,
1073
- baseHeaders,
1074
- url: options.url || getStoryblokUrl(options.region),
1075
- maxRetries: options.maxRetries ?? 6,
1076
- baseDelay: options.baseDelay ?? 500,
1077
- freeze: false
1078
- };
1079
- const request = async (path, fetchOptions, attempt = 0, isRateLimitOwner = false) => {
1080
- if (state.freeze && !isRateLimitOwner) {
1081
- if (options?.verbose) {
1082
- console.log(`\u23F3 ${path} - Waiting for rate limit to be resolved`);
1083
- }
1084
- await new Promise((resolve) => {
1085
- const checkFreeze = setInterval(() => {
1086
- if (!state.freeze) {
1087
- clearInterval(checkFreeze);
1088
- resolve();
1089
- }
1090
- }, 50);
1091
- });
1092
- await delay(100 + Math.random() * 400);
1093
- return request(path, fetchOptions, attempt);
1094
- }
1095
- try {
1096
- if (options?.verbose) {
1097
- console.log(`${state.url}/${path} - Attempt ${attempt}`);
1098
- }
1099
- const requestData = {
1100
- path,
1101
- method: fetchOptions?.method || "GET",
1102
- headers: {
1103
- ...state.baseHeaders,
1104
- ...fetchOptions?.headers
1105
- },
1106
- body: fetchOptions?.body
1107
- };
1108
- options?.onRequest?.(requestData);
1109
- const res = await fetch(`${state.url}/${path}`, {
1110
- headers: requestData.headers,
1111
- ...fetchOptions
1112
- });
1113
- let data;
1114
- if (res.status === 204 || res.headers.get("content-length") === "0") {
1115
- data = null;
1116
- } else {
1117
- try {
1118
- data = await res.json();
1119
- } catch {
1120
- throw new FetchError("Non-JSON response", {
1121
- status: res.status,
1122
- statusText: res.statusText,
1123
- data: null
1124
- });
1125
- }
1126
- }
1127
- options?.onResponse?.({
1128
- path,
1129
- method: requestData.method,
1130
- status: res.status,
1131
- data,
1132
- attempt
1133
- });
1134
- if (res.ok) {
1135
- if (options?.verbose) {
1136
- console.log(`\u2705 ${path}`);
1137
- }
1138
- return {
1139
- data,
1140
- attempt
1141
- };
1142
- } else {
1143
- throw new FetchError("Request failed", {
1144
- status: res.status,
1145
- statusText: res.statusText,
1146
- data
1147
- });
1148
- }
1149
- } catch (error) {
1150
- if (error instanceof FetchError) {
1151
- if (error.response.status === 429 && attempt < state.maxRetries) {
1152
- if (options?.verbose) {
1153
- console.log(`\u274C ${path} - Rate limit exceeded`);
1154
- }
1155
- let isOwner = isRateLimitOwner;
1156
- if (!state.freeze) {
1157
- state.freeze = true;
1158
- isOwner = true;
1159
- }
1160
- const waitTime = state.baseDelay * 2 ** attempt + Math.random() * 100;
1161
- await delay(waitTime);
1162
- try {
1163
- const result = await request(path, fetchOptions, attempt + 1, isOwner);
1164
- return result;
1165
- } finally {
1166
- if (isOwner && state.freeze) {
1167
- state.freeze = false;
1168
- }
1169
- }
1170
- }
1171
- throw error;
1172
- }
1173
- if (state.freeze && isRateLimitOwner) {
1174
- state.freeze = false;
1175
- }
1176
- throw new FetchError(error instanceof Error ? error.message : String(error), {
1177
- status: 0,
1178
- statusText: "Network Error",
1179
- data: null
1180
- });
1181
- }
1182
- };
1183
- const get = async (path, fetchOptions) => {
1184
- return request(path, fetchOptions);
1185
- };
1186
- const post = async (path, fetchOptions) => {
1187
- return request(path, { ...fetchOptions, method: "POST" });
1188
- };
1189
- const put = async (path, fetchOptions) => {
1190
- return request(path, { ...fetchOptions, method: "PUT" });
1191
- };
1192
- const _delete = async (path, fetchOptions) => {
1193
- return request(path, { ...fetchOptions, method: "DELETE" });
1194
- };
1195
- instance = {
1196
- uuid: state.uuid,
1197
- get,
1198
- post,
1199
- put,
1200
- delete: _delete,
1201
- dispose: () => {
1202
- instance = null;
1203
- }
1204
- };
1205
- return instance;
1206
- };
1207
- function mapiClient(options) {
1208
- if (!instance) {
1209
- instance = createMapiClient(options ?? {});
1210
- }
1211
- return instance;
1212
- }
1213
-
1214
- const fetchComponents = async (space) => {
1111
+ const fetchComponents = async (spaceId) => {
1215
1112
  try {
1216
1113
  const client = mapiClient();
1217
- const { data } = await client.get(`spaces/${space}/components`, {});
1218
- return data.components;
1114
+ const { data } = await client.components.list({
1115
+ path: {
1116
+ space_id: spaceId
1117
+ },
1118
+ throwOnError: true
1119
+ });
1120
+ return data?.components;
1219
1121
  } catch (error) {
1220
1122
  handleAPIError("pull_components", error);
1221
1123
  }
1222
1124
  };
1223
- const fetchComponent = async (space, componentName) => {
1125
+ const fetchComponent = async (spaceId, componentName) => {
1224
1126
  try {
1225
1127
  const client = mapiClient();
1226
- const { data } = await client.get(`spaces/${space}/components?search=${encodeURIComponent(componentName)}`, {});
1227
- return data.components?.find((c) => c.name === componentName);
1128
+ const { data } = await client.components.list({
1129
+ path: {
1130
+ space_id: spaceId
1131
+ },
1132
+ query: {
1133
+ search: componentName
1134
+ },
1135
+ throwOnError: true
1136
+ });
1137
+ return data?.components?.find((c) => c.name === componentName);
1228
1138
  } catch (error) {
1229
1139
  handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
1230
1140
  }
1231
1141
  };
1232
- const fetchComponentGroups = async (space) => {
1142
+ const fetchComponentGroups = async (spaceId) => {
1233
1143
  try {
1234
1144
  const client = mapiClient();
1235
- const { data } = await client.get(`spaces/${space}/component_groups`);
1236
- return data.component_groups;
1145
+ const { data } = await client.componentFolders.list({
1146
+ path: {
1147
+ space_id: spaceId
1148
+ }
1149
+ });
1150
+ return data?.component_groups;
1237
1151
  } catch (error) {
1238
1152
  handleAPIError("pull_component_groups", error);
1239
1153
  }
1240
1154
  };
1241
- const fetchComponentPresets = async (space) => {
1155
+ const fetchComponentPresets = async (spaceId) => {
1242
1156
  try {
1243
1157
  const client = mapiClient();
1244
- const { data } = await client.get(`spaces/${space}/presets`);
1245
- return data.presets;
1158
+ const { data } = await client.presets.list({
1159
+ path: {
1160
+ space_id: spaceId
1161
+ }
1162
+ });
1163
+ return data?.presets;
1246
1164
  } catch (error) {
1247
1165
  handleAPIError("pull_component_presets", error);
1248
1166
  }
1249
1167
  };
1250
- const fetchComponentInternalTags = async (space) => {
1168
+ const fetchComponentInternalTags = async (spaceId) => {
1251
1169
  try {
1252
1170
  const client = mapiClient();
1253
- const { data } = await client.get(`spaces/${space}/internal_tags`, {});
1254
- return data.internal_tags.filter((tag) => tag.object_type === "component");
1171
+ const { data } = await client.internalTags.list({
1172
+ path: {
1173
+ space_id: spaceId
1174
+ }
1175
+ });
1176
+ return data?.internal_tags?.filter((tag) => tag.object_type === "component");
1255
1177
  } catch (error) {
1256
1178
  handleAPIError("pull_component_internal_tags", error);
1257
1179
  }
@@ -1263,7 +1185,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
1263
1185
  try {
1264
1186
  if (separateFiles) {
1265
1187
  for (const component of components) {
1266
- const sanitizedName = sanitizeFilename(component.name);
1188
+ const sanitizedName = sanitizeFilename(component.name || "");
1267
1189
  const componentFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${suffix}.json` : `${sanitizedName}.json`);
1268
1190
  await saveToFile(componentFilePath, JSON.stringify(component, null, 2));
1269
1191
  const componentPresets = presets.filter((preset) => preset.component_id === component.id);
@@ -1300,10 +1222,15 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
1300
1222
  const pushComponent = async (space, component) => {
1301
1223
  try {
1302
1224
  const client = mapiClient();
1303
- const { data } = await client.post(`spaces/${space}/components`, {
1304
- body: JSON.stringify(component)
1225
+ const { data } = await client.components.create({
1226
+ path: {
1227
+ space_id: space
1228
+ },
1229
+ body: {
1230
+ component
1231
+ }
1305
1232
  });
1306
- return data.component;
1233
+ return data?.component;
1307
1234
  } catch (error) {
1308
1235
  handleAPIError("push_component", error, `Failed to push component ${component.name}`);
1309
1236
  }
@@ -1311,10 +1238,17 @@ const pushComponent = async (space, component) => {
1311
1238
  const updateComponent = async (space, componentId, component) => {
1312
1239
  try {
1313
1240
  const client = mapiClient();
1314
- const { data } = await client.put(`spaces/${space}/components/${componentId}`, {
1315
- body: JSON.stringify(component)
1241
+ const { data } = await client.components.update({
1242
+ path: {
1243
+ space_id: Number(space),
1244
+ component_id: componentId
1245
+ },
1246
+ body: {
1247
+ component
1248
+ },
1249
+ throwOnError: true
1316
1250
  });
1317
- return data.component;
1251
+ return data?.component;
1318
1252
  } catch (error) {
1319
1253
  handleAPIError("update_component", error, `Failed to update component ${component.name}`);
1320
1254
  }
@@ -1329,10 +1263,16 @@ const upsertComponent = async (space, component, existingId) => {
1329
1263
  const pushComponentGroup = async (space, componentGroup) => {
1330
1264
  try {
1331
1265
  const client = mapiClient();
1332
- const { data } = await client.post(`spaces/${space}/component_groups`, {
1333
- body: JSON.stringify(componentGroup)
1266
+ const { data } = await client.componentFolders.create({
1267
+ path: {
1268
+ space_id: Number(space)
1269
+ },
1270
+ body: {
1271
+ component_group: componentGroup
1272
+ },
1273
+ throwOnError: true
1334
1274
  });
1335
- return data.component_group;
1275
+ return data?.component_group;
1336
1276
  } catch (error) {
1337
1277
  handleAPIError("push_component_group", error, `Failed to push component group ${componentGroup.name}`);
1338
1278
  }
@@ -1340,10 +1280,17 @@ const pushComponentGroup = async (space, componentGroup) => {
1340
1280
  const updateComponentGroup = async (space, groupId, componentGroup) => {
1341
1281
  try {
1342
1282
  const client = mapiClient();
1343
- const { data } = await client.put(`spaces/${space}/component_groups/${groupId}`, {
1344
- body: JSON.stringify(componentGroup)
1283
+ const { data } = await client.componentFolders.update({
1284
+ path: {
1285
+ space_id: Number(space),
1286
+ component_group_id: String(groupId)
1287
+ },
1288
+ body: {
1289
+ component_group: componentGroup
1290
+ },
1291
+ throwOnError: true
1345
1292
  });
1346
- return data.component_group;
1293
+ return data?.component_group;
1347
1294
  } catch (error) {
1348
1295
  handleAPIError("update_component_group", error, `Failed to update component group ${componentGroup.name}`);
1349
1296
  }
@@ -1355,41 +1302,57 @@ const upsertComponentGroup = async (space, group, existingId) => {
1355
1302
  return await pushComponentGroup(space, group);
1356
1303
  }
1357
1304
  };
1358
- const pushComponentPreset = async (space, componentPreset) => {
1305
+ const pushComponentPreset = async (space, preset) => {
1359
1306
  try {
1360
1307
  const client = mapiClient();
1361
- const { data } = await client.post(`spaces/${space}/presets`, {
1362
- body: JSON.stringify(componentPreset)
1308
+ const { data } = await client.presets.create({
1309
+ path: {
1310
+ space_id: Number(space)
1311
+ },
1312
+ body: {
1313
+ preset
1314
+ },
1315
+ throwOnError: true
1363
1316
  });
1364
- return data.preset;
1317
+ return data?.preset;
1365
1318
  } catch (error) {
1366
- handleAPIError("push_component_preset", error, `Failed to push component preset ${componentPreset.preset.name}`);
1319
+ handleAPIError("push_component_preset", error, `Failed to push component preset ${preset.name}`);
1367
1320
  }
1368
1321
  };
1369
- const updateComponentPreset = async (space, presetId, componentPreset) => {
1322
+ const updateComponentPreset = async (space, presetId, preset) => {
1370
1323
  try {
1371
1324
  const client = mapiClient();
1372
- const { data } = await client.put(`spaces/${space}/presets/${presetId}`, {
1373
- body: JSON.stringify(componentPreset)
1325
+ const { data } = await client.presets.update({
1326
+ path: {
1327
+ space_id: Number(space),
1328
+ preset_id: presetId
1329
+ },
1330
+ body: {
1331
+ preset
1332
+ },
1333
+ throwOnError: true
1374
1334
  });
1375
- return data.preset;
1335
+ return data?.preset;
1376
1336
  } catch (error) {
1377
- handleAPIError("update_component_preset", error, `Failed to update component preset ${componentPreset.preset.name}`);
1337
+ handleAPIError("update_component_preset", error, `Failed to update component preset ${preset.name}`);
1378
1338
  }
1379
1339
  };
1380
1340
  const upsertComponentPreset = async (space, preset, existingId) => {
1381
1341
  if (existingId) {
1382
- return await updateComponentPreset(space, existingId, { preset });
1342
+ return await updateComponentPreset(space, existingId, preset);
1383
1343
  } else {
1384
- return await pushComponentPreset(space, { preset });
1344
+ return await pushComponentPreset(space, preset);
1385
1345
  }
1386
1346
  };
1387
1347
  const pushComponentInternalTag = async (space, componentInternalTag) => {
1388
1348
  try {
1389
1349
  const client = mapiClient();
1390
- const { data } = await client.post(`spaces/${space}/internal_tags`, {
1391
- method: "POST",
1392
- body: JSON.stringify(componentInternalTag)
1350
+ const { data } = await client.internalTags.create({
1351
+ path: {
1352
+ space_id: Number(space)
1353
+ },
1354
+ body: componentInternalTag,
1355
+ throwOnError: true
1393
1356
  });
1394
1357
  return data.internal_tag;
1395
1358
  } catch (error) {
@@ -1399,9 +1362,13 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
1399
1362
  const updateComponentInternalTag = async (space, tagId, componentInternalTag) => {
1400
1363
  try {
1401
1364
  const client = mapiClient();
1402
- const { data } = await client.put(`spaces/${space}/internal_tags/${tagId}`, {
1403
- method: "PUT",
1404
- body: JSON.stringify(componentInternalTag)
1365
+ const { data } = await client.internalTags.update({
1366
+ path: {
1367
+ space_id: Number(space),
1368
+ internal_tag_id: tagId
1369
+ },
1370
+ body: componentInternalTag,
1371
+ throwOnError: true
1405
1372
  });
1406
1373
  return data.internal_tag;
1407
1374
  } catch (error) {
@@ -1536,7 +1503,9 @@ componentsCommand.command("pull [componentName]").option("-f, --filename <filena
1536
1503
  }
1537
1504
  const { password, region } = state;
1538
1505
  mapiClient({
1539
- token: password,
1506
+ token: {
1507
+ accessToken: password
1508
+ },
1540
1509
  region
1541
1510
  });
1542
1511
  const spinnerGroups = new Spinner({
@@ -2449,7 +2418,6 @@ function createMinimalStubComponent(name) {
2449
2418
  // Will be set by API
2450
2419
  schema: {},
2451
2420
  // Minimal empty schema
2452
- color: null,
2453
2421
  internal_tags_list: [],
2454
2422
  internal_tag_ids: []
2455
2423
  };
@@ -2575,12 +2543,15 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2575
2543
  konsola.br();
2576
2544
  const { password, region } = state;
2577
2545
  let requestCount = 0;
2578
- mapiClient({
2579
- token: password,
2580
- region,
2581
- onRequest: (_request) => {
2582
- requestCount++;
2583
- }
2546
+ const client = mapiClient({
2547
+ token: {
2548
+ accessToken: password
2549
+ },
2550
+ region
2551
+ });
2552
+ client.interceptors.request.use((config) => {
2553
+ requestCount++;
2554
+ return config;
2584
2555
  });
2585
2556
  try {
2586
2557
  const componentsData = await readComponentsFiles({
@@ -2695,18 +2666,43 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2695
2666
  }
2696
2667
  });
2697
2668
 
2698
- const fetchLanguages = async (space, token, region) => {
2669
+ const fetchSpace = async (spaceId) => {
2699
2670
  try {
2700
- const url = getStoryblokUrl(region);
2701
- const response = await customFetch(`${url}/spaces/${space}`, {
2702
- headers: {
2703
- Authorization: token
2671
+ const client = mapiClient();
2672
+ const { data } = await client.spaces.get({
2673
+ path: {
2674
+ space_id: spaceId
2675
+ },
2676
+ throwOnError: true
2677
+ });
2678
+ return data?.space;
2679
+ } catch (error) {
2680
+ handleAPIError("pull_spaces", error, `Failed to fetch space ${spaceId}`);
2681
+ }
2682
+ };
2683
+ const createSpace = async (space) => {
2684
+ try {
2685
+ const client = mapiClient();
2686
+ const { data } = await client.spaces.create({
2687
+ body: {
2688
+ space
2704
2689
  }
2705
2690
  });
2706
- return {
2707
- default_lang_name: response.space.default_lang_name,
2708
- languages: response.space.languages
2709
- };
2691
+ return data?.space;
2692
+ } catch (error) {
2693
+ handleAPIError("create_space", error, `Failed to create space ${space.name}`);
2694
+ }
2695
+ };
2696
+
2697
+ const fetchLanguages = async (spaceId) => {
2698
+ try {
2699
+ const space = await fetchSpace(spaceId);
2700
+ if (space?.default_lang_name !== void 0 && space?.languages?.length) {
2701
+ return {
2702
+ default_lang_name: space?.default_lang_name,
2703
+ languages: space?.languages
2704
+ };
2705
+ }
2710
2706
  } catch (error) {
2711
2707
  handleAPIError("pull_languages", error);
2712
2708
  }
@@ -2741,12 +2737,18 @@ languagesCommand.command("pull").description(`Download your space's languages sc
2741
2737
  return;
2742
2738
  }
2743
2739
  const { password, region } = state;
2740
+ mapiClient({
2741
+ token: {
2742
+ accessToken: password
2743
+ },
2744
+ region
2745
+ });
2744
2746
  const spinner = new Spinner({
2745
2747
  verbose: !isVitest
2746
2748
  });
2747
2749
  try {
2748
2750
  spinner.start(`Fetching ${chalk.hex(colorPalette.LANGUAGES)("languages")}`);
2749
- const internationalization = await fetchLanguages(space, password, region);
2751
+ const internationalization = await fetchLanguages(space);
2750
2752
  if (!internationalization || internationalization.languages?.length === 0) {
2751
2753
  spinner.failed();
2752
2754
  konsola.warn(`No languages found in the space ${space}`, true);
@@ -2819,7 +2821,9 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
2819
2821
  }
2820
2822
  const { password, region } = state;
2821
2823
  mapiClient({
2822
- token: password,
2824
+ token: {
2825
+ accessToken: password
2826
+ },
2823
2827
  region
2824
2828
  });
2825
2829
  const spinner = new Spinner({
@@ -2843,76 +2847,156 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
2843
2847
  }
2844
2848
  });
2845
2849
 
2846
- const fetchStories = async (space, params) => {
2850
+ const fetchStories = async (spaceId, params) => {
2847
2851
  try {
2848
2852
  const client = mapiClient();
2849
- const allStories = [];
2850
- let currentPage = 1;
2851
- let hasMorePages = true;
2852
- const perPage = 100;
2853
- while (hasMorePages) {
2854
- const { filter_query, ...restParams } = params || {};
2855
- const regularParams = new URLSearchParams({
2856
- ...objectToStringParams({ ...restParams, per_page: perPage }),
2857
- ...currentPage > 1 && { page: currentPage.toString() }
2858
- }).toString();
2859
- const queryString = filter_query ? `${regularParams ? `${regularParams}&` : ""}${filter_query}` : regularParams;
2860
- const endpoint = `spaces/${space}/stories${queryString ? `?${queryString}` : ""}`;
2861
- const { data } = await client.get(endpoint, {});
2862
- allStories.push(...data.stories);
2863
- hasMorePages = data.stories.length === perPage && data.stories.length > 0;
2864
- if (data.stories.length < perPage) {
2865
- break;
2866
- }
2867
- currentPage++;
2868
- }
2869
- return allStories;
2853
+ const { data, response } = await client.stories.list({
2854
+ path: {
2855
+ space_id: spaceId
2856
+ },
2857
+ query: {
2858
+ ...params,
2859
+ per_page: params?.per_page || 100,
2860
+ page: params?.page || 1
2861
+ },
2862
+ throwOnError: true
2863
+ });
2864
+ return {
2865
+ stories: data?.stories || [],
2866
+ headers: response.headers
2867
+ };
2870
2868
  } catch (error) {
2871
2869
  handleAPIError("pull_stories", error);
2872
2870
  }
2873
2871
  };
2874
- async function fetchStoriesByComponent(spaceOptions, filterOptions) {
2875
- const { spaceId } = spaceOptions;
2876
- const { componentName = "", query, starts_with } = filterOptions || {};
2877
- const params = {
2878
- ...starts_with && { starts_with }
2879
- };
2880
- if (componentName) {
2881
- params.contain_component = componentName;
2882
- }
2883
- if (query) {
2884
- params.filter_query = query.startsWith("filter_query") ? query : `filter_query${query}`;
2885
- }
2886
- try {
2887
- const stories = await fetchStories(spaceId, params);
2888
- return stories ?? [];
2889
- } catch (error) {
2890
- handleAPIError("pull_stories", error);
2891
- }
2892
- }
2893
- const fetchStory = async (space, storyId) => {
2872
+ const fetchStory = async (spaceId, storyId) => {
2894
2873
  try {
2895
2874
  const client = mapiClient();
2896
- const endpoint = `spaces/${space}/stories/${storyId}`;
2897
- const { data } = await client.get(endpoint, {});
2898
- return data.story;
2875
+ const { data } = await client.stories.get({
2876
+ path: {
2877
+ space_id: spaceId,
2878
+ story_id: storyId
2879
+ },
2880
+ throwOnError: true
2881
+ });
2882
+ return data?.story;
2899
2883
  } catch (error) {
2900
2884
  handleAPIError("pull_story", error);
2901
2885
  }
2902
2886
  };
2903
- const updateStory = async (space, storyId, payload) => {
2887
+ const updateStory = async (spaceId, storyId, payload) => {
2904
2888
  try {
2905
2889
  const client = mapiClient();
2906
- const endpoint = `spaces/${space}/stories/${storyId}`;
2907
- const { data } = await client.put(endpoint, {
2908
- body: JSON.stringify(payload)
2890
+ const { data } = await client.stories.updateStory({
2891
+ path: {
2892
+ space_id: spaceId,
2893
+ story_id: storyId
2894
+ },
2895
+ body: {
2896
+ story: payload.story,
2897
+ force_update: payload.force_update === "1" ? "1" : "0",
2898
+ publish: payload.publish
2899
+ },
2900
+ throwOnError: true
2909
2901
  });
2910
- return data.story;
2902
+ return data?.story;
2911
2903
  } catch (error) {
2912
2904
  handleAPIError("update_story", error);
2913
2905
  }
2914
2906
  };
2915
2907
 
2908
+ async function* storiesIterator(spaceId, params, onTotal) {
2909
+ try {
2910
+ let perPage = 500;
2911
+ const transformedParams = {
2912
+ ...params
2913
+ };
2914
+ if (params?.componentName && typeof params.componentName === "string") {
2915
+ transformedParams.contain_component = params.componentName;
2916
+ delete transformedParams.componentName;
2917
+ }
2918
+ if (params?.query && typeof params.query === "string") {
2919
+ transformedParams.filter_query = params.query.startsWith("filter_query") ? params.query : `filter_query${params.query}`;
2920
+ delete transformedParams.query;
2921
+ }
2922
+ const result = await fetchStories(spaceId, {
2923
+ ...transformedParams,
2924
+ per_page: perPage,
2925
+ page: 1
2926
+ });
2927
+ if (!result) {
2928
+ return;
2929
+ }
2930
+ const { headers } = result;
2931
+ const total = Number(headers.get("Total"));
2932
+ perPage = Number(headers.get("Per-Page"));
2933
+ const totalPages = Math.ceil(Number(total) / perPage);
2934
+ if (onTotal) {
2935
+ onTotal(total);
2936
+ }
2937
+ for (let page = 1; page <= totalPages; page++) {
2938
+ const result2 = await fetchStories(spaceId, {
2939
+ ...transformedParams,
2940
+ per_page: perPage,
2941
+ page
2942
+ });
2943
+ if (!result2) {
2944
+ return;
2945
+ }
2946
+ const { stories } = result2;
2947
+ for (const story of stories) {
2948
+ yield story;
2949
+ }
2950
+ }
2951
+ } catch (error) {
2952
+ handleAPIError("pull_stories", error);
2953
+ }
2954
+ }
2955
+ class StoriesStream extends Transform {
2956
+ constructor(spaceId, batchSize, onProgress) {
2957
+ super({
2958
+ objectMode: true
2959
+ });
2960
+ this.spaceId = spaceId;
2961
+ this.batchSize = batchSize;
2962
+ this.onProgress = onProgress;
2963
+ this.semaphore = new Sema(this.batchSize, {
2964
+ capacity: this.batchSize
2965
+ });
2966
+ }
2967
+ semaphore;
2968
+ async _transform(chunk, _encoding, callback) {
2969
+ await this.semaphore.acquire();
2970
+ fetchStory(this.spaceId, chunk.id.toString()).then((story) => {
2971
+ this.push(story);
2972
+ this.onProgress?.();
2973
+ }).finally(() => {
2974
+ this.semaphore.release();
2975
+ });
2976
+ callback();
2977
+ }
2978
+ _flush(callback) {
2979
+ this.semaphore.drain().then(() => {
2980
+ callback();
2981
+ });
2982
+ }
2983
+ }
2984
+ const createStoriesStream = async ({
2985
+ spaceId,
2986
+ params,
2987
+ batchSize = 100,
2988
+ onTotal,
2989
+ onProgress
2990
+ }) => {
2991
+ const iterator = storiesIterator(spaceId, params, onTotal);
2992
+ const listStoriesStream = Readable.from(iterator);
2993
+ return pipeline(listStoriesStream, new StoriesStream(spaceId, batchSize, onProgress), (err) => {
2994
+ if (err) {
2995
+ console.error(err);
2996
+ }
2997
+ });
2998
+ };
2999
+
2916
3000
  async function readJavascriptFile(filePath) {
2917
3001
  try {
2918
3002
  const content = await readFile$1(filePath, "utf-8");
@@ -3064,149 +3148,283 @@ async function readRollbackFile({
3064
3148
  }
3065
3149
  }
3066
3150
 
3067
- async function handleMigrations({
3068
- migrationFiles,
3069
- stories,
3070
- space,
3071
- path,
3072
- componentName
3073
- }) {
3074
- const results = {
3075
- successful: [],
3076
- failed: [],
3077
- skipped: []
3078
- };
3079
- const relevantMigrations = componentName ? migrationFiles.filter((file) => {
3080
- const targetComponent = getComponentNameFromFilename(file.name);
3081
- return targetComponent.split(".")[0] === componentName;
3082
- }) : migrationFiles;
3083
- for (const migrationFile of relevantMigrations) {
3084
- const validStories = stories.filter((story) => story.content);
3085
- if (validStories.length === 0) {
3086
- continue;
3087
- }
3088
- await saveRollbackData({
3089
- space,
3090
- path,
3091
- stories: validStories,
3092
- migrationFile: migrationFile.name
3151
+ class MigrationStream extends Transform {
3152
+ constructor(options) {
3153
+ super({
3154
+ objectMode: true
3093
3155
  });
3094
- const migrationFunction = await getMigrationFunction(migrationFile.name, space, path);
3095
- if (!migrationFunction) {
3096
- stories.forEach((story) => {
3097
- results.failed.push({
3156
+ this.options = options;
3157
+ this.results = {
3158
+ successful: [],
3159
+ failed: [],
3160
+ skipped: [],
3161
+ totalProcessed: 0
3162
+ };
3163
+ }
3164
+ results;
3165
+ migrationFunctions = /* @__PURE__ */ new Map();
3166
+ totalProcessed = 0;
3167
+ _transform(chunk, _encoding, callback) {
3168
+ try {
3169
+ this.processStory(chunk).then((results) => {
3170
+ this.results.totalProcessed++;
3171
+ this.options.onProgress?.(this.results.totalProcessed);
3172
+ if (results.length > 0) {
3173
+ this.totalProcessed += results.length;
3174
+ this.options.onTotal?.(this.totalProcessed);
3175
+ for (const result of results) {
3176
+ this.push(result);
3177
+ }
3178
+ }
3179
+ });
3180
+ callback();
3181
+ } catch (error) {
3182
+ callback(error);
3183
+ }
3184
+ }
3185
+ async processStory(story) {
3186
+ if (!story.content) {
3187
+ for (const migrationFile of this.options.migrationFiles) {
3188
+ this.results.failed.push({
3098
3189
  storyId: story.id,
3099
3190
  migrationName: migrationFile.name,
3100
- error: new Error(`Failed to load migration function from file "${migrationFile.name}"`)
3191
+ error: new Error("Story content is missing")
3101
3192
  });
3102
- });
3103
- continue;
3193
+ }
3194
+ return [];
3195
+ }
3196
+ const relevantMigrations = this.options.componentName ? this.options.migrationFiles.filter((file) => {
3197
+ const targetComponent = getComponentNameFromFilename(file.name);
3198
+ return targetComponent.split(".")[0] === this.options.componentName;
3199
+ }) : this.options.migrationFiles;
3200
+ const successfulResults = [];
3201
+ for (const migrationFile of relevantMigrations) {
3202
+ const result = await this.applyMigrationToStory(story, migrationFile);
3203
+ if (result) {
3204
+ successfulResults.push(result);
3205
+ }
3104
3206
  }
3105
- const targetComponent = componentName || getComponentNameFromFilename(migrationFile.name);
3106
- for (const story of stories) {
3107
- if (!story.content) {
3108
- results.failed.push({
3207
+ return successfulResults;
3208
+ }
3209
+ async applyMigrationToStory(story, migrationFile) {
3210
+ try {
3211
+ let migrationFunction = this.migrationFunctions.get(migrationFile.name);
3212
+ if (!migrationFunction) {
3213
+ migrationFunction = await getMigrationFunction(
3214
+ migrationFile.name,
3215
+ this.options.space,
3216
+ this.options.path
3217
+ );
3218
+ this.migrationFunctions.set(migrationFile.name, migrationFunction);
3219
+ }
3220
+ if (!migrationFunction) {
3221
+ this.results.failed.push({
3109
3222
  storyId: story.id,
3110
3223
  migrationName: migrationFile.name,
3111
- error: new Error("Story content is missing")
3224
+ error: new Error(`Failed to load migration function from file "${migrationFile.name}"`)
3112
3225
  });
3113
- continue;
3226
+ return null;
3114
3227
  }
3228
+ await saveRollbackData({
3229
+ space: this.options.space,
3230
+ path: this.options.path,
3231
+ stories: [{ id: story.id, name: story.name || "", content: story.content }],
3232
+ migrationFile: migrationFile.name
3233
+ });
3115
3234
  const storyContent = structuredClone(story.content);
3116
3235
  const originalContentHash = hash(story.content);
3117
- try {
3118
- const modified = applyMigrationToAllBlocks(storyContent, migrationFunction, targetComponent);
3119
- const newContentHash = hash(storyContent);
3120
- const contentChanged = originalContentHash !== newContentHash;
3121
- if (modified && contentChanged) {
3122
- const spinner = new Spinner({ verbose: !isVitest });
3123
- spinner.start(`Applying migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile.name)} to story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}...`);
3124
- spinner.succeed(`Migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile.name)} applied to story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
3125
- results.successful.push({
3126
- storyId: story.id,
3127
- name: story.name,
3128
- migrationName: migrationFile.name,
3129
- content: storyContent
3130
- });
3131
- } else if (modified && !contentChanged) {
3132
- results.skipped.push({
3133
- storyId: story.id,
3134
- name: story.name,
3135
- migrationName: migrationFile.name,
3136
- reason: "No changes detected after migration"
3137
- });
3138
- } else {
3139
- const baseComponent = targetComponent.split(".")[0];
3140
- results.skipped.push({
3141
- storyId: story.id,
3142
- name: story.name,
3143
- migrationName: migrationFile.name,
3144
- reason: baseComponent === componentName ? "No matching components found" : "Different component target"
3145
- });
3146
- }
3147
- } catch (error) {
3148
- const spinner = new Spinner({ verbose: !isVitest });
3149
- spinner.start(`Applying migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile.name)} to story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}...`);
3150
- spinner.failed(`Failed to apply migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile.name)} to story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}`);
3151
- results.failed.push({
3236
+ const targetComponent = this.options.componentName || getComponentNameFromFilename(migrationFile.name);
3237
+ const modified = applyMigrationToAllBlocks(storyContent, migrationFunction, targetComponent);
3238
+ const newContentHash = hash(storyContent);
3239
+ const contentChanged = originalContentHash !== newContentHash;
3240
+ if (modified && contentChanged) {
3241
+ this.results.successful.push({
3242
+ storyId: story.id,
3243
+ name: story.name,
3244
+ migrationName: migrationFile.name,
3245
+ content: storyContent
3246
+ });
3247
+ return {
3248
+ storyId: story.id,
3249
+ name: story.name,
3250
+ content: storyContent
3251
+ };
3252
+ } else if (modified && !contentChanged) {
3253
+ this.results.skipped.push({
3254
+ storyId: story.id,
3255
+ name: story.name,
3256
+ migrationName: migrationFile.name,
3257
+ reason: "No changes detected after migration"
3258
+ });
3259
+ return null;
3260
+ } else {
3261
+ const baseComponent = targetComponent.split(".")[0];
3262
+ this.results.skipped.push({
3152
3263
  storyId: story.id,
3264
+ name: story.name,
3153
3265
  migrationName: migrationFile.name,
3154
- error
3266
+ reason: baseComponent === this.options.componentName ? "No matching components found" : "Different component target"
3155
3267
  });
3268
+ return null;
3156
3269
  }
3270
+ } catch (error) {
3271
+ this.results.failed.push({
3272
+ storyId: story.id,
3273
+ migrationName: migrationFile.name,
3274
+ error
3275
+ });
3276
+ return null;
3157
3277
  }
3158
3278
  }
3159
- return results;
3160
- }
3161
- function summarizeMigrationResults(results) {
3162
- const { successful, failed, skipped } = results;
3163
- const successfulStoryIds = new Set(successful.map((result) => result.storyId));
3164
- const failedStoryIds = new Set(failed.map((result) => result.storyId));
3165
- const successfulMigrations = new Set(successful.map((result) => result.migrationName));
3166
- konsola.br();
3167
- konsola.ok(`Successfully applied ${successfulMigrations.size} migrations to ${successfulStoryIds.size} stories`, true);
3168
- const skippedByReason = skipped.reduce((acc, item) => {
3169
- if (!acc[item.reason]) {
3170
- acc[item.reason] = [];
3171
- }
3172
- acc[item.reason].push(item);
3173
- return acc;
3174
- }, {});
3175
- if (Object.keys(skippedByReason).length > 0) {
3176
- konsola.info(`Skipped migrations:`);
3177
- for (const [reason, items] of Object.entries(skippedByReason)) {
3178
- const uniqueStories = new Set(items.map((item) => item.storyId));
3179
- konsola.info(` \u2022 ${reason}: ${uniqueStories.size} stories`);
3180
- }
3181
- }
3182
- if (failed.length > 0) {
3183
- konsola.warn(`- Failed to apply migrations to ${failedStoryIds.size} stories`, true);
3184
- const failuresByStory = /* @__PURE__ */ new Map();
3185
- failed.forEach(({ storyId, migrationName, error }) => {
3186
- if (!failuresByStory.has(storyId)) {
3187
- failuresByStory.set(storyId, []);
3188
- }
3189
- failuresByStory.get(storyId)?.push({ migrationName, error });
3190
- });
3191
- failuresByStory.forEach((failures, storyId) => {
3192
- konsola.error(`Story ID ${storyId}:`);
3193
- failures.forEach(({ migrationName, error }) => {
3194
- konsola.error(`- Migration ${migrationName}: ${error.message}`);
3195
- });
3196
- });
3197
- } else {
3198
- konsola.ok(`No failures reported`);
3279
+ _flush(callback) {
3280
+ callback();
3281
+ }
3282
+ /**
3283
+ * Get the migration results
3284
+ */
3285
+ getResults() {
3286
+ return this.results;
3287
+ }
3288
+ /**
3289
+ * Get a summary of the migration results
3290
+ */
3291
+ getSummary() {
3292
+ const { successful, failed, skipped } = this.results;
3293
+ const successfulStoryIds = new Set(successful.map((result) => result.storyId));
3294
+ let summary = `Migration Results: ${successfulStoryIds.size} stories updated, ${skipped.length} stories skipped`;
3295
+ if (skipped.length > 0) {
3296
+ const skippedByReason = skipped.reduce((acc, item) => {
3297
+ if (!acc[item.reason]) {
3298
+ acc[item.reason] = 0;
3299
+ }
3300
+ acc[item.reason]++;
3301
+ return acc;
3302
+ }, {});
3303
+ const skippedReasons = Object.entries(skippedByReason).map(([reason, count]) => `${reason}: ${count}`).join(", ");
3304
+ summary += ` (${skippedReasons})`;
3305
+ }
3306
+ if (failed.length > 0) {
3307
+ const failedStoryIds = new Set(failed.map((result) => result.storyId));
3308
+ summary += `, ${failedStoryIds.size} stories failed`;
3309
+ }
3310
+ summary += `.`;
3311
+ return summary;
3199
3312
  }
3200
- konsola.br();
3201
3313
  }
3202
3314
 
3203
3315
  const isStoryPublishedWithoutChanges = (story) => {
3204
- return story.published && !story.unpublished_changes;
3316
+ return true;
3205
3317
  };
3206
3318
  const isStoryWithUnpublishedChanges = (story) => {
3207
- return story.published && story.unpublished_changes;
3319
+ return story.unpublished_changes;
3208
3320
  };
3209
3321
 
3322
+ class UpdateStream extends Writable {
3323
+ constructor(options) {
3324
+ super({
3325
+ objectMode: true
3326
+ });
3327
+ this.options = options;
3328
+ this.batchSize = options.batchSize || 10;
3329
+ this.results = {
3330
+ successful: [],
3331
+ failed: [],
3332
+ totalProcessed: 0
3333
+ };
3334
+ this.semaphore = new Sema(this.batchSize, {
3335
+ capacity: this.batchSize
3336
+ });
3337
+ }
3338
+ results;
3339
+ batchSize;
3340
+ semaphore;
3341
+ async _write(chunk, _encoding, callback) {
3342
+ try {
3343
+ await this.semaphore.acquire();
3344
+ this.updateStory(chunk).finally(() => {
3345
+ this.semaphore.release();
3346
+ });
3347
+ callback();
3348
+ } catch (error) {
3349
+ callback(error);
3350
+ }
3351
+ }
3352
+ async updateStory(migrationResult) {
3353
+ const { storyId, name, content } = migrationResult;
3354
+ const storyName = name || storyId.toString();
3355
+ try {
3356
+ const payload = {
3357
+ story: {
3358
+ content,
3359
+ id: storyId,
3360
+ name: storyName
3361
+ },
3362
+ force_update: "1"
3363
+ };
3364
+ if (this.options.publish === "published" && isStoryPublishedWithoutChanges({ published: true, unpublished_changes: false })) {
3365
+ payload.publish = 1;
3366
+ } else if (this.options.publish === "published-with-changes" && isStoryWithUnpublishedChanges({ published: true, unpublished_changes: true })) {
3367
+ payload.publish = 1;
3368
+ } else if (this.options.publish === "all") {
3369
+ payload.publish = 1;
3370
+ }
3371
+ const updatedStory = await updateStory(this.options.space, storyId, payload);
3372
+ if (updatedStory) {
3373
+ this.results.successful.push({ storyId, name: storyName });
3374
+ this.results.totalProcessed++;
3375
+ this.options.onProgress?.(this.results.totalProcessed);
3376
+ } else {
3377
+ this.results.failed.push({
3378
+ storyId,
3379
+ name: storyName,
3380
+ error: new Error("Update returned null")
3381
+ });
3382
+ this.results.totalProcessed++;
3383
+ this.options.onProgress?.(this.results.totalProcessed);
3384
+ }
3385
+ } catch (error) {
3386
+ this.results.failed.push({
3387
+ storyId,
3388
+ name: storyName,
3389
+ error
3390
+ });
3391
+ this.results.totalProcessed++;
3392
+ this.options.onProgress?.(this.results.totalProcessed);
3393
+ }
3394
+ }
3395
+ async _destroy(error, callback) {
3396
+ try {
3397
+ await this.semaphore.drain();
3398
+ callback();
3399
+ } catch (batchError) {
3400
+ callback(batchError);
3401
+ return;
3402
+ }
3403
+ callback(error);
3404
+ }
3405
+ /**
3406
+ * Get the update results
3407
+ */
3408
+ getResults() {
3409
+ return this.results;
3410
+ }
3411
+ /**
3412
+ * Get a summary of the update results
3413
+ */
3414
+ getSummary() {
3415
+ const { successful, failed, totalProcessed } = this.results;
3416
+ if (totalProcessed === 0) {
3417
+ return `No stories required updates.`;
3418
+ }
3419
+ let summary = `Update Results: ${successful.length} stories updated`;
3420
+ if (failed.length > 0) {
3421
+ summary += `, ${failed.length} stories failed`;
3422
+ }
3423
+ summary += `.`;
3424
+ return summary;
3425
+ }
3426
+ }
3427
+
3210
3428
  const program$8 = getProgram();
3211
3429
  migrationsCommand.command("run [componentName]").description("Run migrations").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("-d, --dry-run", "Preview changes without applying them to 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/"').option("--publish <publish>", "Options for publication mode: all | published | published-with-changes").action(async (componentName, options) => {
3212
3430
  konsola.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
@@ -3224,7 +3442,9 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3224
3442
  }
3225
3443
  const { password, region } = state;
3226
3444
  mapiClient({
3227
- token: password,
3445
+ token: {
3446
+ accessToken: password
3447
+ },
3228
3448
  region
3229
3449
  });
3230
3450
  try {
@@ -3248,119 +3468,76 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3248
3468
  return;
3249
3469
  }
3250
3470
  spinner.succeed(`Found ${filteredMigrations.length} migration files.`);
3251
- const storiesSpinner = new Spinner({ verbose: !isVitest }).start(`Fetching stories...`);
3252
- const stories = await fetchStoriesByComponent(
3253
- {
3254
- spaceId: space
3255
- },
3256
- // Filter options
3257
- {
3471
+ const multiBar = new MultiBar({
3472
+ clearOnComplete: false,
3473
+ format: `${chalk.bold(" {title} ")} ${chalk.hex(colorPalette.PRIMARY)("[{bar}]")} {percentage}% | {eta_formatted} | {value}/{total} processed`,
3474
+ etaBuffer: 60
3475
+ }, Presets.rect);
3476
+ const storiesProgress = multiBar.create(0, 0, {
3477
+ title: "Fetching Stories...".padEnd(19)
3478
+ });
3479
+ const migrationsProgress = multiBar.create(0, 0, {
3480
+ title: "Applying Migrations".padEnd(19)
3481
+ });
3482
+ const updateProgress = multiBar.create(0, 0, {
3483
+ title: "Updating Stories...".padEnd(19)
3484
+ });
3485
+ const storiesStream = await createStoriesStream({
3486
+ spaceId: space,
3487
+ params: {
3258
3488
  componentName,
3259
3489
  query,
3260
3490
  starts_with: startsWith
3491
+ },
3492
+ batchSize: 100,
3493
+ onTotal: (total) => {
3494
+ storiesProgress.setTotal(total);
3495
+ migrationsProgress.setTotal(total);
3496
+ },
3497
+ onProgress: () => {
3498
+ storiesProgress.increment();
3261
3499
  }
3262
- );
3263
- if (!stories || stories.length === 0) {
3264
- storiesSpinner.failed(`No stories found${componentName ? ` for component "${componentName}"` : ""}.`);
3265
- return;
3266
- }
3267
- const storiesWithContent = await Promise.all(stories.map(async (story) => {
3268
- const fullStory = await fetchStory(space, story.id.toString());
3269
- return {
3270
- ...story,
3271
- content: fullStory?.content
3272
- };
3273
- }));
3274
- const validStories = storiesWithContent.filter((story) => story.content);
3275
- const filterParts = [];
3276
- if (componentName) {
3277
- filterParts.push(`component "${componentName}"`);
3278
- }
3279
- if (startsWith) {
3280
- filterParts.push(chalk.hex(colorPalette.PRIMARY)(`starts_with=${startsWith}`));
3281
- }
3282
- if (query) {
3283
- filterParts.push(chalk.hex(colorPalette.PRIMARY)(`filter_query=${query}`));
3284
- }
3285
- const filterMessage = filterParts.length > 0 ? ` (filtered by ${filterParts.join(" and ")})` : "";
3286
- storiesSpinner.succeed(`Fetched ${validStories.length} ${validStories.length === 1 ? "story" : "stories"} with related content${filterMessage}.`);
3287
- const processingSpinner = new Spinner({ verbose: !isVitest }).start(`Processing migrations...`);
3288
- processingSpinner.succeed(`Starting to process ${validStories.length} stories with ${filteredMigrations.length} migrations...`);
3289
- const migrationResults = await handleMigrations({
3500
+ });
3501
+ const migrationStream = new MigrationStream({
3290
3502
  migrationFiles: filteredMigrations,
3291
- stories: validStories,
3292
3503
  space,
3293
3504
  path,
3294
3505
  componentName,
3295
- password,
3296
- region
3506
+ onTotal: (total) => {
3507
+ updateProgress.setTotal(total);
3508
+ },
3509
+ onProgress: () => {
3510
+ migrationsProgress.increment();
3511
+ }
3297
3512
  });
3298
- summarizeMigrationResults(migrationResults);
3299
- if (migrationResults.successful.length > 0 && !dryRun) {
3300
- const updateSpinner = new Spinner({ verbose: !isVitest }).start(`Updating stories in Storyblok...`);
3301
- const storiesByIdMap = /* @__PURE__ */ new Map();
3302
- migrationResults.successful.forEach((result) => {
3303
- const originalStory = validStories.find((s) => s.id === result.storyId);
3304
- storiesByIdMap.set(result.storyId, {
3305
- id: result.storyId,
3306
- name: result.name,
3307
- content: result.content,
3308
- published: originalStory?.published,
3309
- published_at: originalStory?.published_at || void 0,
3310
- unpublished_changes: originalStory?.unpublished_changes
3311
- });
3312
- });
3313
- const storiesToUpdate = Array.from(storiesByIdMap.values());
3314
- if (storiesToUpdate.length === 0) {
3315
- updateSpinner.succeed(`No stories need to be updated in Storyblok.`);
3316
- } else {
3317
- updateSpinner.succeed(`Found ${storiesToUpdate.length} stories to update.`);
3318
- let successCount = 0;
3319
- let failCount = 0;
3320
- for (const story of storiesToUpdate) {
3321
- const storySpinner = new Spinner({ verbose: !isVitest }).start(`Updating story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}...`);
3322
- const payload = {
3323
- story: {
3324
- content: story.content,
3325
- id: story.id,
3326
- name: story.name
3327
- },
3328
- force_update: "1"
3329
- };
3330
- if (publish === "published" && isStoryPublishedWithoutChanges(story)) {
3331
- payload.publish = 1;
3332
- }
3333
- if (publish === "published-with-changes" && isStoryWithUnpublishedChanges(story)) {
3334
- payload.publish = 1;
3335
- }
3336
- if (publish === "all") {
3337
- payload.publish = 1;
3338
- }
3339
- try {
3340
- const updatedStory = await updateStory(space, story.id, payload);
3341
- if (updatedStory) {
3342
- successCount++;
3343
- storySpinner.succeed(`Updated story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())} - Completed in ${storySpinner.elapsedTime.toFixed(2)}ms`);
3344
- } else {
3345
- failCount++;
3346
- storySpinner.failed(`Failed to update story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}`);
3347
- }
3348
- } catch (error) {
3349
- failCount++;
3350
- storySpinner.failed(`Failed to update story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}: ${error.message}`);
3513
+ const updateStream = new UpdateStream({
3514
+ space,
3515
+ publish,
3516
+ dryRun,
3517
+ batchSize: 100,
3518
+ onProgress: () => {
3519
+ updateProgress.increment();
3520
+ }
3521
+ });
3522
+ return new Promise((resolve, reject) => {
3523
+ pipeline(
3524
+ storiesStream,
3525
+ migrationStream,
3526
+ updateStream,
3527
+ (err) => {
3528
+ if (err) {
3529
+ reject(err);
3530
+ return;
3351
3531
  }
3532
+ multiBar.stop();
3533
+ const migrationSummary = migrationStream.getSummary();
3534
+ konsola.info(migrationSummary);
3535
+ const updateSummary = updateStream.getSummary();
3536
+ konsola.info(updateSummary);
3537
+ resolve();
3352
3538
  }
3353
- if (failCount > 0) {
3354
- konsola.warn(`Updated ${successCount} stories successfully, ${failCount} failed.`);
3355
- } else if (successCount > 0) {
3356
- konsola.ok(`Successfully updated ${successCount} stories in Storyblok.`, true);
3357
- }
3358
- }
3359
- } else if (dryRun) {
3360
- konsola.info(`Dry run mode: No stories were updated in Storyblok.`);
3361
- } else if (migrationResults.successful.length === 0) {
3362
- konsola.info(`No stories were modified by the migrations.`);
3363
- }
3539
+ );
3540
+ });
3364
3541
  } catch (error) {
3365
3542
  handleError(error, verbose);
3366
3543
  }
@@ -3913,7 +4090,7 @@ const getComponentType = (componentName, options) => {
3913
4090
  const prefix = options.typePrefix ?? "";
3914
4091
  const suffix = options.typeSuffix ?? "";
3915
4092
  const sanitizedName = componentName.replace(/[^a-z0-9]/gi, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
3916
- const componentType = toPascalCase(toCamelCase(`${prefix}_${sanitizedName}_${suffix}`));
4093
+ const componentType = toPascalCase(`${prefix}_${sanitizedName}_${suffix}`);
3917
4094
  const isFirstCharacterNumber = !Number.isNaN(Number.parseInt(componentType.charAt(0)));
3918
4095
  return isFirstCharacterNumber ? `_${componentType}` : componentType;
3919
4096
  };
@@ -3923,34 +4100,38 @@ const getComponentPropertiesTypeAnnotations = async (component, options, spaceDa
3923
4100
  if (key.startsWith("tab-")) {
3924
4101
  return acc;
3925
4102
  }
3926
- const propertyType = value.type;
4103
+ if (!value || typeof value !== "object" || !("type" in value)) {
4104
+ return acc;
4105
+ }
4106
+ const schema = value;
4107
+ const propertyType = schema.type;
3927
4108
  const propertyTypeAnnotation = {
3928
- [key]: getPropertyTypeAnnotation(value, options.typePrefix, options.typeSuffix)
4109
+ [key]: getPropertyTypeAnnotation(schema, options.typePrefix, options.typeSuffix)
3929
4110
  };
3930
4111
  if (propertyType === "custom" && customFieldsParser) {
3931
- const customField = typeof customFieldsParser === "function" ? customFieldsParser(key, value) : {};
4112
+ const customField = typeof customFieldsParser === "function" ? customFieldsParser(key, schema) : {};
3932
4113
  return {
3933
4114
  ...acc,
3934
4115
  ...customField
3935
4116
  };
3936
4117
  }
3937
4118
  if (Array.from(storyblokSchemas.keys()).includes(propertyType)) {
3938
- const componentType = toPascalCase(toCamelCase(propertyType));
4119
+ const componentType = toPascalCase(propertyType);
3939
4120
  propertyTypeAnnotation[key].tsType = `Storyblok${componentType}`;
3940
4121
  }
3941
4122
  if (propertyType === "multilink") {
3942
4123
  const excludedLinktypes = [
3943
- ...!value.email_link_type ? ['{ linktype?: "email" }'] : [],
3944
- ...!value.asset_link_type ? ['{ linktype?: "asset" }'] : []
4124
+ ...!schema.email_link_type ? ['{ linktype?: "email" }'] : [],
4125
+ ...!schema.asset_link_type ? ['{ linktype?: "asset" }'] : []
3945
4126
  ];
3946
- const componentType = toPascalCase(toCamelCase(propertyType));
3947
- propertyTypeAnnotation[key].tsType = excludedLinktypes.length > 0 ? `Exclude<Storyblok${componentType}, ${excludedLinktypes.join(" | ")}>` : componentType;
4127
+ const componentType = `Storyblok${toPascalCase(propertyType)}`;
4128
+ propertyTypeAnnotation[key].tsType = excludedLinktypes.length > 0 ? `Exclude<${componentType}, ${excludedLinktypes.join(" | ")}>` : componentType;
3948
4129
  }
3949
4130
  if (propertyType === "bloks") {
3950
- if (value.restrict_components) {
3951
- if (value.restrict_type === "groups") {
3952
- if (Array.isArray(value.component_group_whitelist) && value.component_group_whitelist.length > 0) {
3953
- const componentsInGroupWhitelist = value.component_group_whitelist.reduce(
4131
+ if (schema.restrict_components) {
4132
+ if (schema.restrict_type === "groups") {
4133
+ if (Array.isArray(schema.component_group_whitelist) && schema.component_group_whitelist.length > 0) {
4134
+ const componentsInGroupWhitelist = schema.component_group_whitelist.reduce(
3954
4135
  (components, groupUUID) => {
3955
4136
  const componentsInGroup = spaceData.components.filter(
3956
4137
  (component2) => component2.component_group_uuid === groupUUID
@@ -3964,18 +4145,18 @@ const getComponentPropertiesTypeAnnotations = async (component, options, spaceDa
3964
4145
  );
3965
4146
  propertyTypeAnnotation[key].tsType = componentsInGroupWhitelist.length > 0 ? `(${componentsInGroupWhitelist.join(" | ")})[]` : `never[]`;
3966
4147
  }
3967
- } else if (value.restrict_type === "tags") {
3968
- if (Array.isArray(value.component_tag_whitelist) && value.component_tag_whitelist.length > 0) {
4148
+ } else if (schema.restrict_type === "tags") {
4149
+ if (Array.isArray(schema.component_tag_whitelist) && schema.component_tag_whitelist.length > 0) {
3969
4150
  const componentsWithTags = spaceData.components.filter(
3970
4151
  (component2) => component2.internal_tag_ids && component2.internal_tag_ids.some(
3971
- (tagId) => value.component_tag_whitelist.includes(Number(tagId))
4152
+ (tagId) => schema.component_tag_whitelist.includes(Number(tagId))
3972
4153
  )
3973
4154
  );
3974
4155
  propertyTypeAnnotation[key].tsType = componentsWithTags.length > 0 ? `(${componentsWithTags.map((component2) => getComponentType(component2.name, options)).join(" | ")})[]` : `never[]`;
3975
4156
  }
3976
4157
  } else {
3977
- if (Array.isArray(value.component_whitelist) && value.component_whitelist.length > 0) {
3978
- propertyTypeAnnotation[key].tsType = `(${value.component_whitelist.map((name) => getComponentType(name, options)).join(" | ")})[]`;
4158
+ if (Array.isArray(schema.component_whitelist) && schema.component_whitelist.length > 0) {
4159
+ propertyTypeAnnotation[key].tsType = `(${schema.component_whitelist.map((name) => getComponentType(name, options)).join(" | ")})[]`;
3979
4160
  }
3980
4161
  }
3981
4162
  } else {
@@ -4024,7 +4205,7 @@ const generateTypes = async (spaceData, options = {
4024
4205
  const componentPropertiesTypeAnnotations = await getComponentPropertiesTypeAnnotations(component, options, spaceData, customFieldsParser);
4025
4206
  const requiredFields = Object.entries(component?.schema || {}).reduce(
4026
4207
  (acc, [key, value]) => {
4027
- if (value.required) {
4208
+ if (value && typeof value === "object" && "required" in value && value.required) {
4028
4209
  return [...acc, key];
4029
4210
  }
4030
4211
  return acc;
@@ -4163,41 +4344,65 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4163
4344
  const program$4 = getProgram();
4164
4345
  const datasourcesCommand = program$4.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").hook("preAction", resolveRegion);
4165
4346
 
4166
- const fetchDatasourceEntries = async (space, datasourceId) => {
4347
+ const fetchDatasourceEntries = async (spaceId, datasourceId) => {
4167
4348
  try {
4168
4349
  const client = mapiClient();
4169
- const { data } = await client.get(`spaces/${space}/datasource_entries?datasource_id=${datasourceId}`);
4170
- return data.datasource_entries;
4350
+ const { data } = await client.datasourceEntries.list({
4351
+ path: {
4352
+ space_id: spaceId
4353
+ },
4354
+ query: {
4355
+ datasource_id: datasourceId
4356
+ },
4357
+ throwOnError: true
4358
+ });
4359
+ return data?.datasource_entries;
4171
4360
  } catch (error) {
4172
4361
  handleAPIError("pull_datasources", error);
4173
4362
  }
4174
4363
  };
4175
- const fetchDatasources = async (space) => {
4364
+ const fetchDatasources = async (spaceId) => {
4176
4365
  try {
4177
4366
  const client = mapiClient();
4178
- const { data } = await client.get(`spaces/${space}/datasources`);
4179
- const datasources = data.datasources;
4367
+ const { data } = await client.datasources.list({
4368
+ path: {
4369
+ space_id: spaceId
4370
+ },
4371
+ throwOnError: true
4372
+ });
4373
+ const datasources = data?.datasources;
4180
4374
  const datasourcesWithEntries = await Promise.all(
4181
- datasources.map(async (ds) => {
4182
- const entries = await fetchDatasourceEntries(space, ds.id);
4375
+ datasources?.map(async (ds) => {
4376
+ if (!ds.id) {
4377
+ return { ...ds, entries: [] };
4378
+ }
4379
+ const entries = await fetchDatasourceEntries(spaceId, ds.id);
4183
4380
  return { ...ds, entries };
4184
- })
4381
+ }) || []
4185
4382
  );
4186
4383
  return datasourcesWithEntries;
4187
4384
  } catch (error) {
4188
4385
  handleAPIError("pull_datasources", error);
4189
4386
  }
4190
4387
  };
4191
- const fetchDatasource = async (space, datasourceName) => {
4388
+ const fetchDatasource = async (spaceId, datasourceName) => {
4192
4389
  try {
4193
4390
  const client = mapiClient();
4194
- const { data } = await client.get(`spaces/${space}/datasources?search=${encodeURIComponent(datasourceName)}`);
4195
- const found = data.datasources?.find((d) => d.name === datasourceName);
4391
+ const { data } = await client.datasources.list({
4392
+ path: {
4393
+ space_id: spaceId
4394
+ },
4395
+ query: {
4396
+ search: datasourceName
4397
+ },
4398
+ throwOnError: true
4399
+ });
4400
+ const found = data?.datasources?.find((d) => d.name === datasourceName);
4196
4401
  if (!found) {
4197
4402
  return void 0;
4198
4403
  }
4199
- const entries = await fetchDatasourceEntries(space, found.id);
4200
- return { ...found, entries };
4404
+ const entries = await fetchDatasourceEntries(spaceId, found.id);
4405
+ return { ...found, entries: entries || [] };
4201
4406
  } catch (error) {
4202
4407
  handleAPIError("pull_datasources", error, `Failed to fetch datasource ${datasourceName}`);
4203
4408
  }
@@ -4208,7 +4413,7 @@ const saveDatasourcesToFiles = async (space, datasources, options) => {
4208
4413
  try {
4209
4414
  if (separateFiles) {
4210
4415
  for (const datasource of datasources) {
4211
- const sanitizedName = sanitizeFilename(datasource.name);
4416
+ const sanitizedName = sanitizeFilename(datasource.name || "");
4212
4417
  const datasourceFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${suffix}.json` : `${sanitizedName}.json`);
4213
4418
  await saveToFile(datasourceFilePath, JSON.stringify(datasource, null, 2));
4214
4419
  }
@@ -4238,7 +4443,9 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
4238
4443
  }
4239
4444
  const { password, region } = state;
4240
4445
  mapiClient({
4241
- token: password,
4446
+ token: {
4447
+ accessToken: password
4448
+ },
4242
4449
  region
4243
4450
  });
4244
4451
  const spinnerDatasources = new Spinner({
@@ -4291,22 +4498,33 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
4291
4498
  }
4292
4499
  });
4293
4500
 
4294
- const pushDatasource = async (space, datasource) => {
4501
+ const pushDatasource = async (spaceId, datasource) => {
4295
4502
  try {
4296
4503
  const client = mapiClient();
4297
- const { data } = await client.post(`spaces/${space}/datasources`, {
4298
- body: JSON.stringify(datasource)
4504
+ const { data } = await client.datasources.create({
4505
+ path: {
4506
+ space_id: spaceId
4507
+ },
4508
+ body: { datasource },
4509
+ throwOnError: true
4299
4510
  });
4300
4511
  return data.datasource;
4301
4512
  } catch (error) {
4302
4513
  handleAPIError("push_datasource", error, `Failed to push datasource ${datasource.name}`);
4303
4514
  }
4304
4515
  };
4305
- const updateDatasource = async (space, datasourceId, datasource) => {
4516
+ const updateDatasource = async (spaceId, datasourceId, datasource) => {
4306
4517
  try {
4307
4518
  const client = mapiClient();
4308
- const { data } = await client.put(`spaces/${space}/datasources/${datasourceId}`, {
4309
- body: JSON.stringify(datasource)
4519
+ const { data } = await client.datasources.update({
4520
+ path: {
4521
+ space_id: spaceId,
4522
+ datasource_id: datasourceId
4523
+ },
4524
+ body: {
4525
+ datasource
4526
+ },
4527
+ throwOnError: true
4310
4528
  });
4311
4529
  return data.datasource;
4312
4530
  } catch (error) {
@@ -4320,29 +4538,38 @@ const upsertDatasource = async (space, datasource, existingId) => {
4320
4538
  return await pushDatasource(space, datasource);
4321
4539
  }
4322
4540
  };
4323
- const pushDatasourceEntry = async (space, datasourceId, entry) => {
4541
+ const pushDatasourceEntry = async (spaceId, datasourceId, entry) => {
4324
4542
  try {
4325
4543
  const client = mapiClient();
4326
- const { data } = await client.post(`spaces/${space}/datasource_entries`, {
4327
- body: JSON.stringify({
4544
+ const { data } = await client.datasourceEntries.create({
4545
+ path: {
4546
+ space_id: spaceId
4547
+ },
4548
+ body: {
4328
4549
  datasource_entry: {
4329
4550
  ...entry,
4330
4551
  datasource_id: datasourceId
4331
4552
  }
4332
- })
4553
+ },
4554
+ throwOnError: true
4333
4555
  });
4334
4556
  return data.datasource_entry;
4335
4557
  } catch (error) {
4336
4558
  handleAPIError("push_datasource", error, `Failed to push datasource entry ${entry.name}`);
4337
4559
  }
4338
4560
  };
4339
- const updateDatasourceEntry = async (space, entryId, entry) => {
4561
+ const updateDatasourceEntry = async (spaceId, entryId, entry) => {
4340
4562
  try {
4341
4563
  const client = mapiClient();
4342
- await client.put(`spaces/${space}/datasource_entries/${entryId}`, {
4343
- body: JSON.stringify({
4564
+ await client.datasourceEntries.updateDatasourceEntry({
4565
+ path: {
4566
+ space_id: spaceId,
4567
+ datasource_entry_id: entryId
4568
+ },
4569
+ body: {
4344
4570
  datasource_entry: entry
4345
- })
4571
+ },
4572
+ throwOnError: true
4346
4573
  });
4347
4574
  } catch (error) {
4348
4575
  handleAPIError("update_datasource", error, `Failed to update datasource entry ${entry.name}`);
@@ -4447,7 +4674,9 @@ datasourcesCommand.command("push [datasourceName]").description(`Push your space
4447
4674
  konsola.br();
4448
4675
  const { password, region } = state;
4449
4676
  mapiClient({
4450
- token: password,
4677
+ token: {
4678
+ accessToken: password
4679
+ },
4451
4680
  region
4452
4681
  });
4453
4682
  try {
@@ -4506,8 +4735,7 @@ datasourcesCommand.command("push [datasourceName]").description(`Push your space
4506
4735
  for (const entry of entries) {
4507
4736
  const existingEntryId = existingDatasource?.entries?.find((e) => e.name === entry.name)?.id;
4508
4737
  try {
4509
- const { id, ...entryData } = entry;
4510
- await upsertDatasourceEntry(space, result.id, entryData, existingEntryId);
4738
+ await upsertDatasourceEntry(space, result.id, entry, existingEntryId);
4511
4739
  } catch (entryError) {
4512
4740
  results.failed.push({ name: datasource.name, error: entryError });
4513
4741
  spinner.failed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Failed in ${spinner.elapsedTime.toFixed(2)}ms`);
@@ -4535,12 +4763,18 @@ datasourcesCommand.command("push [datasourceName]").description(`Push your space
4535
4763
  }
4536
4764
  });
4537
4765
 
4538
- async function deleteDatasource(space, id) {
4766
+ async function deleteDatasource(spaceId, id) {
4539
4767
  try {
4540
4768
  const client = mapiClient();
4541
- await client.delete(`spaces/${space}/datasources/${id}`);
4769
+ await client.datasources.delete({
4770
+ path: {
4771
+ space_id: spaceId,
4772
+ datasource_id: Number(id)
4773
+ },
4774
+ throwOnError: true
4775
+ });
4542
4776
  } catch (error) {
4543
- handleAPIError("delete_datasource", error, `Datasource with id '${id}' not found in space ${space}.`);
4777
+ handleAPIError("delete_datasource", error, `Datasource with id '${id}' not found in space ${spaceId}.`);
4544
4778
  }
4545
4779
  }
4546
4780
 
@@ -4568,7 +4802,9 @@ datasourcesCommand.command("delete [name]").description("Delete a datasource fro
4568
4802
  }
4569
4803
  const { password, region } = state;
4570
4804
  mapiClient({
4571
- token: password,
4805
+ token: {
4806
+ accessToken: password
4807
+ },
4572
4808
  region
4573
4809
  });
4574
4810
  const spinner = new Spinner({
@@ -4716,7 +4952,8 @@ const repositoryToTemplate = (repo) => {
4716
4952
  template: repo.clone_url,
4717
4953
  location: port ? `https://localhost:${port}/` : "https://localhost:3000/",
4718
4954
  description: repo.description,
4719
- updated_at: repo.updated_at
4955
+ updated_at: repo.updated_at,
4956
+ stars: repo.stargazers_count
4720
4957
  };
4721
4958
  };
4722
4959
  const fetchBlueprintRepositories = async () => {
@@ -4728,25 +4965,13 @@ const fetchBlueprintRepositories = async () => {
4728
4965
  order: "desc",
4729
4966
  per_page: 100
4730
4967
  });
4731
- const blueprints = data.items.filter((repo) => repo.name.startsWith("blueprint-core-")).map(repositoryToTemplate).sort((a, b) => a.name.localeCompare(b.name));
4968
+ const blueprints = data.items.filter((repo) => repo.name.startsWith("blueprint-core-")).map(repositoryToTemplate).sort((a, b) => (b.stars || 0) - (a.stars || 0));
4732
4969
  return blueprints;
4733
4970
  } catch (error) {
4734
4971
  handleAPIError("fetch_blueprints", error, "Failed to fetch blueprints from GitHub");
4735
4972
  }
4736
4973
  };
4737
4974
 
4738
- const createSpace = async (space) => {
4739
- try {
4740
- const client = mapiClient();
4741
- const { data } = await client.post("spaces", {
4742
- body: JSON.stringify(space)
4743
- });
4744
- return data.space;
4745
- } catch (error) {
4746
- handleAPIError("create_space", error, `Failed to create space ${space.name}`);
4747
- }
4748
- };
4749
-
4750
4975
  const program$1 = getProgram();
4751
4976
  program$1.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").action(async (projectPath, options) => {
4752
4977
  konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
@@ -4766,7 +4991,9 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
4766
4991
  }
4767
4992
  const { password, region } = state;
4768
4993
  mapiClient({
4769
- token: password,
4994
+ token: {
4995
+ accessToken: password
4996
+ },
4770
4997
  region
4771
4998
  });
4772
4999
  const spinnerBlueprints = new Spinner({
@@ -4777,7 +5004,10 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
4777
5004
  });
4778
5005
  let userData;
4779
5006
  try {
4780
- const { user } = await getUser(password, region);
5007
+ const user = await getUser(password, region);
5008
+ if (!user) {
5009
+ throw new Error("User data is undefined");
5010
+ }
4781
5011
  userData = user;
4782
5012
  } catch (error) {
4783
5013
  konsola.error("Failed to fetch user info. Please login again.", error);
@@ -4843,7 +5073,7 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
4843
5073
  { name: "My personal account", value: "personal" }
4844
5074
  ];
4845
5075
  if (userData.has_org) {
4846
- choices.push({ name: `Organization (${userData.org.name})`, value: "org" });
5076
+ choices.push({ name: `Organization (${userData?.org?.name})`, value: "org" });
4847
5077
  }
4848
5078
  if (userData.has_partner) {
4849
5079
  choices.push({ name: "Partner Portal", value: "partner" });
@@ -4910,7 +5140,7 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
4910
5140
  konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyTemplate)} project is ready \u{1F389} !`);
4911
5141
  if (createdSpace?.first_token) {
4912
5142
  if (whereToCreateSpace === "org") {
4913
- konsola.ok(`Storyblok space created in organization ${chalk.hex(colorPalette.PRIMARY)(userData.org.name)}, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
5143
+ konsola.ok(`Storyblok space created in organization ${chalk.hex(colorPalette.PRIMARY)(userData?.org?.name)}, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
4914
5144
  } else if (whereToCreateSpace === "partner") {
4915
5145
  konsola.ok(`Storyblok space created in partner portal, preview url and .env configured automatically. You can now open your space in the browser at ${chalk.hex(colorPalette.PRIMARY)(generateSpaceUrl(createdSpace.id, region))}`);
4916
5146
  } else {
@@ -4922,7 +5152,8 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
4922
5152
  cd ${finalProjectPath}
4923
5153
  npm install
4924
5154
  npm run dev
4925
- `);
5155
+ `);
5156
+ konsola.info(`Or check the dedicated guide at: ${chalk.hex(colorPalette.PRIMARY)(`https://www.storyblok.com/docs/guides/${technologyTemplate}`)}`);
4926
5157
  } catch (error) {
4927
5158
  spinnerSpace.failed();
4928
5159
  spinnerBlueprints.failed();
@@ -4932,7 +5163,7 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
4932
5163
  konsola.br();
4933
5164
  });
4934
5165
 
4935
- const version = "4.4.1";
5166
+ const version = "4.6.0";
4936
5167
  const pkg = {
4937
5168
  version: version};
4938
5169