storyblok 3.29.1 → 3.30.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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/dist/cli.mjs +91 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -250,6 +250,10 @@ $ storyblok sync --type <COMMAND> --source <SPACE_ID> --target <SPACE_ID>
250
250
  * `type`: describe the command type to execute. Can be: `folders`, `components`, `stories`, `datasources` or `roles`. It's possible pass multiple types separated by comma (`,`).
251
251
  * `source`: the source space to use to sync
252
252
  * `target`: the target space to use to sync
253
+ * `starts-with`: sync only stories that starts with the given string
254
+ * `filter`: sync stories based on the given filter. Required Options: Required options: `--keys`, `--operations`, `--values`
255
+ * `keys`: Multiple keys should be separated by comma. Example: `--keys key1,key2`, `--keys key1`
256
+ * `operations`: Operations to be used for filtering. Can be: `is`, `in`, `not_in`, `like`, `not_like`, `any_in_array`, `all_in_array`, `gt_date`, `lt_date`, `gt_int`, `lt_int`, `gt_float`, `lt_float`. Multiple operations should be separated by comma.
253
257
 
254
258
  #### Examples
255
259
 
@@ -260,6 +264,15 @@ $ storyblok sync --type components --source 00001 --target 00002
260
264
  # Sync components and stories from `00001` space to `00002` space
261
265
  $ storyblok sync --type components,stories --source 00001 --target 00002
262
266
 
267
+ # Sync only stories that starts with `myStartsWithString` from `00001` space to `00002` space
268
+ $ storyblok sync --type stories --source 00001 --target 00002 --starts-with myStartsWithString
269
+
270
+ # Sync only stories with a category field like `reference` from `00001` space to `00002` space
271
+ $ storyblok sync --type stories --source 00001 --target 00002 --filter --keys category --operations like --values reference
272
+
273
+ # Sync only stories with a category field like `reference` and a name field not like `demo` from `00001` space to `00002` space
274
+ $ storyblok sync --type stories --source 00001 --target 00002 --filter --keys category,name --operations like,not_like --values reference,demo
275
+
263
276
  ```
264
277
 
265
278
  ### quickstart
package/dist/cli.mjs CHANGED
@@ -784,6 +784,28 @@ const saveFileFactory = async (fileName, content, path = "./") => {
784
784
  });
785
785
  };
786
786
 
787
+ const buildFilterQuery = (keys, operations, values) => {
788
+ const operators = ["is", "in", "not_in", "like", "not_like", "any_in_array", "all_in_array", "gt_date", "lt_date", "gt_int", "lt_int", "gt_float", "lt_float"];
789
+ if (!keys || !operations || !values) {
790
+ throw new Error("Filter options are required: --keys; --operations; --values");
791
+ }
792
+ const _keys = keys.split(",");
793
+ const _operations = operations.split(",");
794
+ const _values = values.split(",");
795
+ if (_keys.length !== _operations.length || _keys.length !== _values.length) {
796
+ throw new Error("The number of keys, operations and values must be the same");
797
+ }
798
+ const invalidOperators = _operations.filter((o) => !operators.includes(o));
799
+ if (invalidOperators.length) {
800
+ throw new Error("Invalid operator(s) applied for filter: " + invalidOperators.join(" "));
801
+ }
802
+ const filterQuery = {};
803
+ _keys.forEach((key, index) => {
804
+ filterQuery[key] = { [_operations[index]]: _values[index] };
805
+ });
806
+ return filterQuery;
807
+ };
808
+
787
809
  class SyncComponentGroups {
788
810
  /**
789
811
  * @param {{ sourceSpaceId: string, targetSpaceId: string, oauthToken: string }} options
@@ -1463,11 +1485,13 @@ const SyncSpaces = {
1463
1485
  init(options) {
1464
1486
  const { api } = options;
1465
1487
  console.log(chalk.green("\u2713") + " Loading options");
1488
+ this.client = api.getClient();
1466
1489
  this.sourceSpaceId = options.source;
1467
1490
  this.targetSpaceId = options.target;
1468
1491
  this.oauthToken = options.token;
1469
- this.client = api.getClient();
1470
1492
  this.componentsGroups = options._componentsGroups;
1493
+ this.startsWith = options.startsWith;
1494
+ this.filterQuery = options.filterQuery;
1471
1495
  },
1472
1496
  async getStoryWithTranslatedSlugs(sourceStory, targetStory) {
1473
1497
  const storyForPayload = { ...sourceStory };
@@ -1492,64 +1516,78 @@ const SyncSpaces = {
1492
1516
  }
1493
1517
  return storyForPayload;
1494
1518
  },
1495
- async syncStories() {
1496
- console.log(chalk.green("\u2713") + " Syncing stories...");
1519
+ async getTargetFolders() {
1497
1520
  const targetFolders = await this.client.getAll(`spaces/${this.targetSpaceId}/stories`, {
1498
1521
  folder_only: 1,
1499
1522
  sort_by: "slug:asc"
1500
1523
  });
1501
1524
  const folderMapping = {};
1502
- for (let i = 0; i < targetFolders.length; i++) {
1503
- var folder = targetFolders[i];
1525
+ for (const folder of targetFolders) {
1504
1526
  folderMapping[folder.full_slug] = folder.id;
1505
1527
  }
1506
- const all = await this.client.getAll(`spaces/${this.sourceSpaceId}/stories`, {
1507
- story_only: 1
1528
+ return folderMapping;
1529
+ },
1530
+ async updateStoriesAndFolders(data, targetContent = null, sourceContent = null, isFolder = false) {
1531
+ let createdStory = null;
1532
+ const contentName = sourceContent.name;
1533
+ const contentTypeName = isFolder ? "Folder" : "Story";
1534
+ const payload = {
1535
+ story: data,
1536
+ force_update: "1",
1537
+ ...!isFolder && sourceContent.published ? { publish: 1 } : {}
1538
+ };
1539
+ if (targetContent) {
1540
+ console.log(`${chalk.yellow("-")} ${contentTypeName} ${contentName} already exists`);
1541
+ createdStory = await this.client.put(`spaces/${this.targetSpaceId}/stories/${targetContent.id}`, payload);
1542
+ console.log(`${chalk.green("\u2713")} ${contentTypeName} ${targetContent.full_slug} updated`);
1543
+ } else {
1544
+ createdStory = await this.client.post(`spaces/${this.targetSpaceId}/stories`, payload);
1545
+ console.log(`${chalk.green("\u2713")} ${contentTypeName} ${sourceContent.full_slug} created`);
1546
+ }
1547
+ createdStory = createdStory.data.story;
1548
+ if (createdStory.uuid !== sourceContent.uuid) {
1549
+ await this.client.put(`spaces/${this.targetSpaceId}/stories/${createdStory.id}/update_uuid`, { uuid: sourceContent.uuid });
1550
+ }
1551
+ return createdStory;
1552
+ },
1553
+ async syncStories() {
1554
+ console.log(chalk.green("\u2713") + " Syncing stories...");
1555
+ const folderMapping = { ...await this.getTargetFolders() };
1556
+ const allStories = await this.client.getAll(`spaces/${this.sourceSpaceId}/stories`, {
1557
+ story_only: 1,
1558
+ ...this.startsWith ? { starts_with: this.startsWith } : {},
1559
+ ...this.filterQuery ? { filter_query: this.filterQuery } : {}
1508
1560
  });
1509
- for (let i = 0; i < all.length; i++) {
1510
- console.log(chalk.green("\u2713") + " Starting update " + all[i].full_slug);
1511
- const { data } = await this.client.get("spaces/" + this.sourceSpaceId + "/stories/" + all[i].id);
1561
+ for (const story of allStories) {
1562
+ console.log(chalk.green("\u2713") + " Starting update " + story.full_slug);
1563
+ const { data } = await this.client.get(`spaces/${this.sourceSpaceId}/stories/${story.id}`);
1512
1564
  const sourceStory = data.story;
1513
1565
  const slugs = sourceStory.full_slug.split("/");
1514
1566
  let folderId = 0;
1515
1567
  if (slugs.length > 1) {
1516
1568
  slugs.pop();
1517
- var folderSlug = slugs.join("/");
1569
+ const folderSlug = slugs.join("/");
1518
1570
  if (folderMapping[folderSlug]) {
1519
1571
  folderId = folderMapping[folderSlug];
1520
1572
  } else {
1521
- console.error(chalk.red("X") + "The folder does not exist " + folderSlug);
1573
+ console.error(`${chalk.red("X")} The folder does not exist ${folderSlug}`);
1522
1574
  continue;
1523
1575
  }
1524
1576
  }
1525
1577
  sourceStory.parent_id = folderId;
1526
1578
  try {
1527
- const existingStory = await this.client.get("spaces/" + this.targetSpaceId + "/stories", { with_slug: all[i].full_slug });
1528
- const storyData = await this.getStoryWithTranslatedSlugs(sourceStory, existingStory.data.stories ? existingStory.data.stories[0] : null);
1529
- const payload = {
1530
- story: storyData,
1531
- force_update: "1",
1532
- ...sourceStory.published ? { publish: 1 } : {}
1533
- };
1534
- let createdStory = null;
1535
- if (existingStory.data.stories.length === 1) {
1536
- createdStory = await this.client.put("spaces/" + this.targetSpaceId + "/stories/" + existingStory.data.stories[0].id, payload);
1537
- console.log(chalk.green("\u2713") + " Updated " + existingStory.data.stories[0].full_slug);
1538
- } else {
1539
- createdStory = await this.client.post("spaces/" + this.targetSpaceId + "/stories", payload);
1540
- console.log(chalk.green("\u2713") + " Created " + sourceStory.full_slug);
1541
- }
1542
- if (createdStory.data.story.uuid !== sourceStory.uuid) {
1543
- await this.client.put("spaces/" + this.targetSpaceId + "/stories/" + createdStory.data.story.id + "/update_uuid", { uuid: sourceStory.uuid });
1544
- }
1579
+ const { data: data2 } = await this.client.get("spaces/" + this.targetSpaceId + "/stories", { with_slug: story.full_slug });
1580
+ const existingStory = data2.stories[0];
1581
+ const storyData = await this.getStoryWithTranslatedSlugs(sourceStory, existingStory ? existingStory[0] : null);
1582
+ await this.updateStoriesAndFolders(storyData, existingStory, sourceStory);
1545
1583
  } catch (e) {
1546
1584
  console.error(
1547
- chalk.red("X") + ` Story ${all[i].name} Sync failed: ${e.message}`
1585
+ chalk.red("X") + ` Story ${story.name} Sync failed: ${e.message}`
1548
1586
  );
1549
1587
  console.log(e);
1550
1588
  }
1551
1589
  }
1552
- return Promise.resolve(all);
1590
+ return Promise.resolve(allStories);
1553
1591
  },
1554
1592
  async syncFolders() {
1555
1593
  console.log(chalk.green("\u2713") + " Syncing folders...");
@@ -1558,13 +1596,12 @@ const SyncSpaces = {
1558
1596
  sort_by: "slug:asc"
1559
1597
  });
1560
1598
  const syncedFolders = {};
1561
- for (var i = 0; i < sourceFolders.length; i++) {
1562
- const folder = sourceFolders[i];
1599
+ for (const folder of sourceFolders) {
1563
1600
  try {
1564
- const folderResult = await this.client.get("spaces/" + this.sourceSpaceId + "/stories/" + folder.id);
1565
- const sourceFolder = folderResult.data.story;
1566
- const existingFolder = await this.client.get("spaces/" + this.targetSpaceId + "/stories", { with_slug: folder.full_slug });
1567
- const folderData = await this.getStoryWithTranslatedSlugs(sourceFolder, existingFolder.data.stories ? existingFolder.data.stories[0] : null);
1601
+ const folderResult = await this.client.get(`spaces/${this.sourceSpaceId}/stories/${folder.id}`);
1602
+ const { data } = await this.client.get(`spaces/${this.targetSpaceId}/stories`, { with_slug: folder.full_slug });
1603
+ const existingFolder = data.stories[0] || null;
1604
+ const folderData = await this.getStoryWithTranslatedSlugs(folderResult.data.story, existingFolder);
1568
1605
  delete folderData.id;
1569
1606
  delete folderData.created_at;
1570
1607
  if (folder.parent_id) {
@@ -1583,23 +1620,7 @@ const SyncSpaces = {
1583
1620
  folderData.parent_id = syncedFolders[folder.id];
1584
1621
  }
1585
1622
  }
1586
- const payload = {
1587
- story: folderData,
1588
- force_update: "1"
1589
- };
1590
- let createdFolder = null;
1591
- if (existingFolder.data.stories.length === 1) {
1592
- console.log(`Folder ${folder.name} already exists`);
1593
- createdFolder = await this.client.put("spaces/" + this.targetSpaceId + "/stories/" + existingFolder.data.stories[0].id, payload);
1594
- console.log(chalk.green("\u2713") + ` Folder ${folder.name} updated`);
1595
- } else {
1596
- createdFolder = await this.client.post("spaces/" + this.targetSpaceId + "/stories", payload);
1597
- console.log(chalk.green("\u2713") + ` Folder ${folder.name} created`);
1598
- }
1599
- if (createdFolder.data.story.uuid !== folder.uuid) {
1600
- await this.client.put("spaces/" + this.targetSpaceId + "/stories/" + createdFolder.data.story.id + "/update_uuid", { uuid: folder.uuid });
1601
- }
1602
- syncedFolders[folder.id] = createdFolder.data.story.id;
1623
+ await this.updateStoriesAndFolders(folderData, existingFolder, folder, true);
1603
1624
  } catch (e) {
1604
1625
  console.error(
1605
1626
  chalk.red("X") + ` Folder ${folder.name} Sync failed: ${e.message}`
@@ -3656,15 +3677,26 @@ program.command(COMMANDS.SELECT).description("Usage to kickstart a boilerplate,
3656
3677
  program.command(COMMANDS.SYNC).description("Sync schemas, roles, folders and stories between spaces").requiredOption(
3657
3678
  "--type <TYPE>",
3658
3679
  "Define what will be sync. Can be components, folders, stories, datasources or roles"
3659
- ).requiredOption("--source <SPACE_ID>", "Source space id").requiredOption("--target <SPACE_ID>", "Target space id").option("--components-groups <UUIDs>", "Synchronize components based on their group UUIDs separated by commas").action(async (options) => {
3680
+ ).requiredOption("--source <SPACE_ID>", "Source space id").requiredOption("--target <SPACE_ID>", "Target space id").option("--starts-with <STARTS_WITH>", "Sync only stories that starts with the given string").option("--filter", "Enable filter options to sync only stories that match the given filter. Required options: --keys; --operations; --values").option("--keys <KEYS>", "Field names in your story object which should be used for filtering. Multiple keys should separated by comma.").option("--operations <OPERATIONS>", "Operations to be used for filtering. Can be: is, in, not_in, like, not_like, any_in_array, all_in_array, gt_date, lt_date, gt_int, lt_int, gt_float, lt_float. Multiple operations should be separated by comma.").option("--values <VALUES>", "Values to be used for filtering. Any string or number. If you want to use multiple values, separate them with a comma. Multiple values should be separated by comma.").option("--components-groups <UUIDs>", "Synchronize components based on their group UUIDs separated by commas").action(async (options) => {
3660
3681
  console.log(`${chalk.blue("-")} Sync data between spaces
3661
3682
  `);
3662
3683
  try {
3663
3684
  if (!api.isAuthorized()) {
3664
3685
  await api.processLogin();
3665
3686
  }
3666
- const { type, target, source, componentsGroups } = options;
3687
+ const {
3688
+ type,
3689
+ target,
3690
+ source,
3691
+ startsWith,
3692
+ filter,
3693
+ keys,
3694
+ operations,
3695
+ values,
3696
+ componentsGroups
3697
+ } = options;
3667
3698
  const _componentsGroups = componentsGroups ? componentsGroups.split(",") : null;
3699
+ const filterQuery = filter ? buildFilterQuery(keys, operations, values) : void 0;
3668
3700
  const token = creds.get().token || null;
3669
3701
  const _types = type.split(",") || [];
3670
3702
  _types.forEach((_type) => {
@@ -3677,6 +3709,8 @@ program.command(COMMANDS.SYNC).description("Sync schemas, roles, folders and sto
3677
3709
  token,
3678
3710
  target,
3679
3711
  source,
3712
+ startsWith,
3713
+ filterQuery,
3680
3714
  _componentsGroups
3681
3715
  });
3682
3716
  console.log("\n" + chalk.green("\u2713") + " Sync data between spaces successfully completed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyblok",
3
- "version": "3.29.1",
3
+ "version": "3.30.0",
4
4
  "description": "A simple CLI to start Storyblok from your command line.",
5
5
  "repository": {
6
6
  "type": "git",