storyblok 3.29.1 → 3.31.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/README.md +14 -0
- package/dist/cli.mjs +103 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -250,6 +250,11 @@ $ 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.
|
|
257
|
+
* `components-full-sync`: If used, the CLI will override the full component object when synching across spaces.
|
|
253
258
|
|
|
254
259
|
#### Examples
|
|
255
260
|
|
|
@@ -260,6 +265,15 @@ $ storyblok sync --type components --source 00001 --target 00002
|
|
|
260
265
|
# Sync components and stories from `00001` space to `00002` space
|
|
261
266
|
$ storyblok sync --type components,stories --source 00001 --target 00002
|
|
262
267
|
|
|
268
|
+
# Sync only stories that starts with `myStartsWithString` from `00001` space to `00002` space
|
|
269
|
+
$ storyblok sync --type stories --source 00001 --target 00002 --starts-with myStartsWithString
|
|
270
|
+
|
|
271
|
+
# Sync only stories with a category field like `reference` from `00001` space to `00002` space
|
|
272
|
+
$ storyblok sync --type stories --source 00001 --target 00002 --filter --keys category --operations like --values reference
|
|
273
|
+
|
|
274
|
+
# Sync only stories with a category field like `reference` and a name field not like `demo` from `00001` space to `00002` space
|
|
275
|
+
$ storyblok sync --type stories --source 00001 --target 00002 --filter --keys category,name --operations like,not_like --values reference,demo
|
|
276
|
+
|
|
263
277
|
```
|
|
264
278
|
|
|
265
279
|
### 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
|
|
@@ -1017,6 +1039,7 @@ class SyncComponents {
|
|
|
1017
1039
|
this.client = api.getClient();
|
|
1018
1040
|
this.presetsLib = new PresetsLib({ oauthToken: options.oauthToken, targetSpaceId: this.targetSpaceId });
|
|
1019
1041
|
this.componentsGroups = options.componentsGroups;
|
|
1042
|
+
this.componentsFullSync = options.componentsFullSync;
|
|
1020
1043
|
}
|
|
1021
1044
|
async sync() {
|
|
1022
1045
|
const syncComponentGroupsInstance = new SyncComponentGroups({
|
|
@@ -1149,7 +1172,10 @@ class SyncComponents {
|
|
|
1149
1172
|
return this.client.put(`spaces/${spaceId}/components/${componentId}`, payload);
|
|
1150
1173
|
}
|
|
1151
1174
|
mergeComponents(sourceComponent, targetComponent = {}) {
|
|
1152
|
-
const data = {
|
|
1175
|
+
const data = this.componentsFullSync ? {
|
|
1176
|
+
// This should be the default behavior in a major future version
|
|
1177
|
+
...sourceComponent
|
|
1178
|
+
} : {
|
|
1153
1179
|
...sourceComponent,
|
|
1154
1180
|
...targetComponent
|
|
1155
1181
|
};
|
|
@@ -1463,11 +1489,14 @@ const SyncSpaces = {
|
|
|
1463
1489
|
init(options) {
|
|
1464
1490
|
const { api } = options;
|
|
1465
1491
|
console.log(chalk.green("\u2713") + " Loading options");
|
|
1492
|
+
this.client = api.getClient();
|
|
1466
1493
|
this.sourceSpaceId = options.source;
|
|
1467
1494
|
this.targetSpaceId = options.target;
|
|
1468
1495
|
this.oauthToken = options.token;
|
|
1469
|
-
this.client = api.getClient();
|
|
1470
1496
|
this.componentsGroups = options._componentsGroups;
|
|
1497
|
+
this.componentsFullSync = options._componentsFullSync;
|
|
1498
|
+
this.startsWith = options.startsWith;
|
|
1499
|
+
this.filterQuery = options.filterQuery;
|
|
1471
1500
|
},
|
|
1472
1501
|
async getStoryWithTranslatedSlugs(sourceStory, targetStory) {
|
|
1473
1502
|
const storyForPayload = { ...sourceStory };
|
|
@@ -1492,64 +1521,78 @@ const SyncSpaces = {
|
|
|
1492
1521
|
}
|
|
1493
1522
|
return storyForPayload;
|
|
1494
1523
|
},
|
|
1495
|
-
async
|
|
1496
|
-
console.log(chalk.green("\u2713") + " Syncing stories...");
|
|
1524
|
+
async getTargetFolders() {
|
|
1497
1525
|
const targetFolders = await this.client.getAll(`spaces/${this.targetSpaceId}/stories`, {
|
|
1498
1526
|
folder_only: 1,
|
|
1499
1527
|
sort_by: "slug:asc"
|
|
1500
1528
|
});
|
|
1501
1529
|
const folderMapping = {};
|
|
1502
|
-
for (
|
|
1503
|
-
var folder = targetFolders[i];
|
|
1530
|
+
for (const folder of targetFolders) {
|
|
1504
1531
|
folderMapping[folder.full_slug] = folder.id;
|
|
1505
1532
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1533
|
+
return folderMapping;
|
|
1534
|
+
},
|
|
1535
|
+
async updateStoriesAndFolders(data, targetContent = null, sourceContent = null, isFolder = false) {
|
|
1536
|
+
let createdStory = null;
|
|
1537
|
+
const contentName = sourceContent.name;
|
|
1538
|
+
const contentTypeName = isFolder ? "Folder" : "Story";
|
|
1539
|
+
const payload = {
|
|
1540
|
+
story: data,
|
|
1541
|
+
force_update: "1",
|
|
1542
|
+
...!isFolder && sourceContent.published ? { publish: 1 } : {}
|
|
1543
|
+
};
|
|
1544
|
+
if (targetContent) {
|
|
1545
|
+
console.log(`${chalk.yellow("-")} ${contentTypeName} ${contentName} already exists`);
|
|
1546
|
+
createdStory = await this.client.put(`spaces/${this.targetSpaceId}/stories/${targetContent.id}`, payload);
|
|
1547
|
+
console.log(`${chalk.green("\u2713")} ${contentTypeName} ${targetContent.full_slug} updated`);
|
|
1548
|
+
} else {
|
|
1549
|
+
createdStory = await this.client.post(`spaces/${this.targetSpaceId}/stories`, payload);
|
|
1550
|
+
console.log(`${chalk.green("\u2713")} ${contentTypeName} ${sourceContent.full_slug} created`);
|
|
1551
|
+
}
|
|
1552
|
+
createdStory = createdStory.data.story;
|
|
1553
|
+
if (createdStory.uuid !== sourceContent.uuid) {
|
|
1554
|
+
await this.client.put(`spaces/${this.targetSpaceId}/stories/${createdStory.id}/update_uuid`, { uuid: sourceContent.uuid });
|
|
1555
|
+
}
|
|
1556
|
+
return createdStory;
|
|
1557
|
+
},
|
|
1558
|
+
async syncStories() {
|
|
1559
|
+
console.log(chalk.green("\u2713") + " Syncing stories...");
|
|
1560
|
+
const folderMapping = { ...await this.getTargetFolders() };
|
|
1561
|
+
const allStories = await this.client.getAll(`spaces/${this.sourceSpaceId}/stories`, {
|
|
1562
|
+
story_only: 1,
|
|
1563
|
+
...this.startsWith ? { starts_with: this.startsWith } : {},
|
|
1564
|
+
...this.filterQuery ? { filter_query: this.filterQuery } : {}
|
|
1508
1565
|
});
|
|
1509
|
-
for (
|
|
1510
|
-
console.log(chalk.green("\u2713") + " Starting update " +
|
|
1511
|
-
const { data } = await this.client.get(
|
|
1566
|
+
for (const story of allStories) {
|
|
1567
|
+
console.log(chalk.green("\u2713") + " Starting update " + story.full_slug);
|
|
1568
|
+
const { data } = await this.client.get(`spaces/${this.sourceSpaceId}/stories/${story.id}`);
|
|
1512
1569
|
const sourceStory = data.story;
|
|
1513
1570
|
const slugs = sourceStory.full_slug.split("/");
|
|
1514
1571
|
let folderId = 0;
|
|
1515
1572
|
if (slugs.length > 1) {
|
|
1516
1573
|
slugs.pop();
|
|
1517
|
-
|
|
1574
|
+
const folderSlug = slugs.join("/");
|
|
1518
1575
|
if (folderMapping[folderSlug]) {
|
|
1519
1576
|
folderId = folderMapping[folderSlug];
|
|
1520
1577
|
} else {
|
|
1521
|
-
console.error(chalk.red("X")
|
|
1578
|
+
console.error(`${chalk.red("X")} The folder does not exist ${folderSlug}`);
|
|
1522
1579
|
continue;
|
|
1523
1580
|
}
|
|
1524
1581
|
}
|
|
1525
1582
|
sourceStory.parent_id = folderId;
|
|
1526
1583
|
try {
|
|
1527
|
-
const
|
|
1528
|
-
const
|
|
1529
|
-
const
|
|
1530
|
-
|
|
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
|
-
}
|
|
1584
|
+
const { data: data2 } = await this.client.get("spaces/" + this.targetSpaceId + "/stories", { with_slug: story.full_slug });
|
|
1585
|
+
const existingStory = data2.stories[0];
|
|
1586
|
+
const storyData = await this.getStoryWithTranslatedSlugs(sourceStory, existingStory ? existingStory[0] : null);
|
|
1587
|
+
await this.updateStoriesAndFolders(storyData, existingStory, sourceStory);
|
|
1545
1588
|
} catch (e) {
|
|
1546
1589
|
console.error(
|
|
1547
|
-
chalk.red("X") + ` Story ${
|
|
1590
|
+
chalk.red("X") + ` Story ${story.name} Sync failed: ${e.message}`
|
|
1548
1591
|
);
|
|
1549
1592
|
console.log(e);
|
|
1550
1593
|
}
|
|
1551
1594
|
}
|
|
1552
|
-
return Promise.resolve(
|
|
1595
|
+
return Promise.resolve(allStories);
|
|
1553
1596
|
},
|
|
1554
1597
|
async syncFolders() {
|
|
1555
1598
|
console.log(chalk.green("\u2713") + " Syncing folders...");
|
|
@@ -1558,13 +1601,12 @@ const SyncSpaces = {
|
|
|
1558
1601
|
sort_by: "slug:asc"
|
|
1559
1602
|
});
|
|
1560
1603
|
const syncedFolders = {};
|
|
1561
|
-
for (
|
|
1562
|
-
const folder = sourceFolders[i];
|
|
1604
|
+
for (const folder of sourceFolders) {
|
|
1563
1605
|
try {
|
|
1564
|
-
const folderResult = await this.client.get(
|
|
1565
|
-
const
|
|
1566
|
-
const existingFolder =
|
|
1567
|
-
const folderData = await this.getStoryWithTranslatedSlugs(
|
|
1606
|
+
const folderResult = await this.client.get(`spaces/${this.sourceSpaceId}/stories/${folder.id}`);
|
|
1607
|
+
const { data } = await this.client.get(`spaces/${this.targetSpaceId}/stories`, { with_slug: folder.full_slug });
|
|
1608
|
+
const existingFolder = data.stories[0] || null;
|
|
1609
|
+
const folderData = await this.getStoryWithTranslatedSlugs(folderResult.data.story, existingFolder);
|
|
1568
1610
|
delete folderData.id;
|
|
1569
1611
|
delete folderData.created_at;
|
|
1570
1612
|
if (folder.parent_id) {
|
|
@@ -1583,23 +1625,7 @@ const SyncSpaces = {
|
|
|
1583
1625
|
folderData.parent_id = syncedFolders[folder.id];
|
|
1584
1626
|
}
|
|
1585
1627
|
}
|
|
1586
|
-
|
|
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;
|
|
1628
|
+
await this.updateStoriesAndFolders(folderData, existingFolder, folder, true);
|
|
1603
1629
|
} catch (e) {
|
|
1604
1630
|
console.error(
|
|
1605
1631
|
chalk.red("X") + ` Folder ${folder.name} Sync failed: ${e.message}`
|
|
@@ -1649,7 +1675,8 @@ const SyncSpaces = {
|
|
|
1649
1675
|
sourceSpaceId: this.sourceSpaceId,
|
|
1650
1676
|
targetSpaceId: this.targetSpaceId,
|
|
1651
1677
|
oauthToken: this.oauthToken,
|
|
1652
|
-
componentsGroups: this.componentsGroups
|
|
1678
|
+
componentsGroups: this.componentsGroups,
|
|
1679
|
+
componentsFullSync: this.componentsFullSync
|
|
1653
1680
|
});
|
|
1654
1681
|
try {
|
|
1655
1682
|
await syncComponentsInstance.sync();
|
|
@@ -3656,15 +3683,28 @@ program.command(COMMANDS.SELECT).description("Usage to kickstart a boilerplate,
|
|
|
3656
3683
|
program.command(COMMANDS.SYNC).description("Sync schemas, roles, folders and stories between spaces").requiredOption(
|
|
3657
3684
|
"--type <TYPE>",
|
|
3658
3685
|
"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) => {
|
|
3686
|
+
).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").option("--components-full-sync", "Synchronize components by overriding any property from source to target").action(async (options) => {
|
|
3660
3687
|
console.log(`${chalk.blue("-")} Sync data between spaces
|
|
3661
3688
|
`);
|
|
3662
3689
|
try {
|
|
3663
3690
|
if (!api.isAuthorized()) {
|
|
3664
3691
|
await api.processLogin();
|
|
3665
3692
|
}
|
|
3666
|
-
const {
|
|
3693
|
+
const {
|
|
3694
|
+
type,
|
|
3695
|
+
target,
|
|
3696
|
+
source,
|
|
3697
|
+
startsWith,
|
|
3698
|
+
filter,
|
|
3699
|
+
keys,
|
|
3700
|
+
operations,
|
|
3701
|
+
values,
|
|
3702
|
+
componentsGroups,
|
|
3703
|
+
componentsFullSync
|
|
3704
|
+
} = options;
|
|
3667
3705
|
const _componentsGroups = componentsGroups ? componentsGroups.split(",") : null;
|
|
3706
|
+
const _componentsFullSync = !!componentsFullSync;
|
|
3707
|
+
const filterQuery = filter ? buildFilterQuery(keys, operations, values) : void 0;
|
|
3668
3708
|
const token = creds.get().token || null;
|
|
3669
3709
|
const _types = type.split(",") || [];
|
|
3670
3710
|
_types.forEach((_type) => {
|
|
@@ -3677,7 +3717,10 @@ program.command(COMMANDS.SYNC).description("Sync schemas, roles, folders and sto
|
|
|
3677
3717
|
token,
|
|
3678
3718
|
target,
|
|
3679
3719
|
source,
|
|
3680
|
-
|
|
3720
|
+
startsWith,
|
|
3721
|
+
filterQuery,
|
|
3722
|
+
_componentsGroups,
|
|
3723
|
+
_componentsFullSync
|
|
3681
3724
|
});
|
|
3682
3725
|
console.log("\n" + chalk.green("\u2713") + " Sync data between spaces successfully completed");
|
|
3683
3726
|
} catch (e) {
|