storyblok 3.29.0 → 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.
- package/README.md +13 -0
- package/dist/cli.mjs +97 -60
- 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
|
@@ -23,6 +23,8 @@ import fs$1 from 'fs-extra';
|
|
|
23
23
|
import csvReader from 'fast-csv';
|
|
24
24
|
import xmlConverter from 'xml-js';
|
|
25
25
|
import { compile } from 'json-schema-to-typescript';
|
|
26
|
+
import { dirname } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
26
28
|
|
|
27
29
|
const getOptions = (subCommand, argv = {}, api = {}) => {
|
|
28
30
|
let email = "";
|
|
@@ -782,6 +784,28 @@ const saveFileFactory = async (fileName, content, path = "./") => {
|
|
|
782
784
|
});
|
|
783
785
|
};
|
|
784
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
|
+
|
|
785
809
|
class SyncComponentGroups {
|
|
786
810
|
/**
|
|
787
811
|
* @param {{ sourceSpaceId: string, targetSpaceId: string, oauthToken: string }} options
|
|
@@ -1461,11 +1485,13 @@ const SyncSpaces = {
|
|
|
1461
1485
|
init(options) {
|
|
1462
1486
|
const { api } = options;
|
|
1463
1487
|
console.log(chalk.green("\u2713") + " Loading options");
|
|
1488
|
+
this.client = api.getClient();
|
|
1464
1489
|
this.sourceSpaceId = options.source;
|
|
1465
1490
|
this.targetSpaceId = options.target;
|
|
1466
1491
|
this.oauthToken = options.token;
|
|
1467
|
-
this.client = api.getClient();
|
|
1468
1492
|
this.componentsGroups = options._componentsGroups;
|
|
1493
|
+
this.startsWith = options.startsWith;
|
|
1494
|
+
this.filterQuery = options.filterQuery;
|
|
1469
1495
|
},
|
|
1470
1496
|
async getStoryWithTranslatedSlugs(sourceStory, targetStory) {
|
|
1471
1497
|
const storyForPayload = { ...sourceStory };
|
|
@@ -1490,64 +1516,78 @@ const SyncSpaces = {
|
|
|
1490
1516
|
}
|
|
1491
1517
|
return storyForPayload;
|
|
1492
1518
|
},
|
|
1493
|
-
async
|
|
1494
|
-
console.log(chalk.green("\u2713") + " Syncing stories...");
|
|
1519
|
+
async getTargetFolders() {
|
|
1495
1520
|
const targetFolders = await this.client.getAll(`spaces/${this.targetSpaceId}/stories`, {
|
|
1496
1521
|
folder_only: 1,
|
|
1497
1522
|
sort_by: "slug:asc"
|
|
1498
1523
|
});
|
|
1499
1524
|
const folderMapping = {};
|
|
1500
|
-
for (
|
|
1501
|
-
var folder = targetFolders[i];
|
|
1525
|
+
for (const folder of targetFolders) {
|
|
1502
1526
|
folderMapping[folder.full_slug] = folder.id;
|
|
1503
1527
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
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 } : {}
|
|
1506
1560
|
});
|
|
1507
|
-
for (
|
|
1508
|
-
console.log(chalk.green("\u2713") + " Starting update " +
|
|
1509
|
-
const { data } = await this.client.get(
|
|
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}`);
|
|
1510
1564
|
const sourceStory = data.story;
|
|
1511
1565
|
const slugs = sourceStory.full_slug.split("/");
|
|
1512
1566
|
let folderId = 0;
|
|
1513
1567
|
if (slugs.length > 1) {
|
|
1514
1568
|
slugs.pop();
|
|
1515
|
-
|
|
1569
|
+
const folderSlug = slugs.join("/");
|
|
1516
1570
|
if (folderMapping[folderSlug]) {
|
|
1517
1571
|
folderId = folderMapping[folderSlug];
|
|
1518
1572
|
} else {
|
|
1519
|
-
console.error(chalk.red("X")
|
|
1573
|
+
console.error(`${chalk.red("X")} The folder does not exist ${folderSlug}`);
|
|
1520
1574
|
continue;
|
|
1521
1575
|
}
|
|
1522
1576
|
}
|
|
1523
1577
|
sourceStory.parent_id = folderId;
|
|
1524
1578
|
try {
|
|
1525
|
-
const
|
|
1526
|
-
const
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
force_update: "1",
|
|
1530
|
-
...sourceStory.published ? { publish: 1 } : {}
|
|
1531
|
-
};
|
|
1532
|
-
let createdStory = null;
|
|
1533
|
-
if (existingStory.data.stories.length === 1) {
|
|
1534
|
-
createdStory = await this.client.put("spaces/" + this.targetSpaceId + "/stories/" + existingStory.data.stories[0].id, payload);
|
|
1535
|
-
console.log(chalk.green("\u2713") + " Updated " + existingStory.data.stories[0].full_slug);
|
|
1536
|
-
} else {
|
|
1537
|
-
createdStory = await this.client.post("spaces/" + this.targetSpaceId + "/stories", payload);
|
|
1538
|
-
console.log(chalk.green("\u2713") + " Created " + sourceStory.full_slug);
|
|
1539
|
-
}
|
|
1540
|
-
if (createdStory.data.story.uuid !== sourceStory.uuid) {
|
|
1541
|
-
await this.client.put("spaces/" + this.targetSpaceId + "/stories/" + createdStory.data.story.id + "/update_uuid", { uuid: sourceStory.uuid });
|
|
1542
|
-
}
|
|
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);
|
|
1543
1583
|
} catch (e) {
|
|
1544
1584
|
console.error(
|
|
1545
|
-
chalk.red("X") + ` Story ${
|
|
1585
|
+
chalk.red("X") + ` Story ${story.name} Sync failed: ${e.message}`
|
|
1546
1586
|
);
|
|
1547
1587
|
console.log(e);
|
|
1548
1588
|
}
|
|
1549
1589
|
}
|
|
1550
|
-
return Promise.resolve(
|
|
1590
|
+
return Promise.resolve(allStories);
|
|
1551
1591
|
},
|
|
1552
1592
|
async syncFolders() {
|
|
1553
1593
|
console.log(chalk.green("\u2713") + " Syncing folders...");
|
|
@@ -1556,13 +1596,12 @@ const SyncSpaces = {
|
|
|
1556
1596
|
sort_by: "slug:asc"
|
|
1557
1597
|
});
|
|
1558
1598
|
const syncedFolders = {};
|
|
1559
|
-
for (
|
|
1560
|
-
const folder = sourceFolders[i];
|
|
1599
|
+
for (const folder of sourceFolders) {
|
|
1561
1600
|
try {
|
|
1562
|
-
const folderResult = await this.client.get(
|
|
1563
|
-
const
|
|
1564
|
-
const existingFolder =
|
|
1565
|
-
const folderData = await this.getStoryWithTranslatedSlugs(
|
|
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);
|
|
1566
1605
|
delete folderData.id;
|
|
1567
1606
|
delete folderData.created_at;
|
|
1568
1607
|
if (folder.parent_id) {
|
|
@@ -1581,23 +1620,7 @@ const SyncSpaces = {
|
|
|
1581
1620
|
folderData.parent_id = syncedFolders[folder.id];
|
|
1582
1621
|
}
|
|
1583
1622
|
}
|
|
1584
|
-
|
|
1585
|
-
story: folderData,
|
|
1586
|
-
force_update: "1"
|
|
1587
|
-
};
|
|
1588
|
-
let createdFolder = null;
|
|
1589
|
-
if (existingFolder.data.stories.length === 1) {
|
|
1590
|
-
console.log(`Folder ${folder.name} already exists`);
|
|
1591
|
-
createdFolder = await this.client.put("spaces/" + this.targetSpaceId + "/stories/" + existingFolder.data.stories[0].id, payload);
|
|
1592
|
-
console.log(chalk.green("\u2713") + ` Folder ${folder.name} updated`);
|
|
1593
|
-
} else {
|
|
1594
|
-
createdFolder = await this.client.post("spaces/" + this.targetSpaceId + "/stories", payload);
|
|
1595
|
-
console.log(chalk.green("\u2713") + ` Folder ${folder.name} created`);
|
|
1596
|
-
}
|
|
1597
|
-
if (createdFolder.data.story.uuid !== folder.uuid) {
|
|
1598
|
-
await this.client.put("spaces/" + this.targetSpaceId + "/stories/" + createdFolder.data.story.id + "/update_uuid", { uuid: folder.uuid });
|
|
1599
|
-
}
|
|
1600
|
-
syncedFolders[folder.id] = createdFolder.data.story.id;
|
|
1623
|
+
await this.updateStoriesAndFolders(folderData, existingFolder, folder, true);
|
|
1601
1624
|
} catch (e) {
|
|
1602
1625
|
console.error(
|
|
1603
1626
|
chalk.red("X") + ` Folder ${folder.name} Sync failed: ${e.message}`
|
|
@@ -3461,7 +3484,8 @@ const tasks = {
|
|
|
3461
3484
|
generateTypescriptTypedefs
|
|
3462
3485
|
};
|
|
3463
3486
|
|
|
3464
|
-
const
|
|
3487
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3488
|
+
const rawPkg = fs.readFileSync(path.join(__dirname, "../package.json"));
|
|
3465
3489
|
const pkg = JSON.parse(rawPkg);
|
|
3466
3490
|
const program = new commander.Command();
|
|
3467
3491
|
const allRegionsText = ALL_REGIONS.join(", ");
|
|
@@ -3547,7 +3571,7 @@ program.command("pull-languages").description("Download your space's languages s
|
|
|
3547
3571
|
program.command(COMMANDS.PULL_COMPONENTS).option("--sf, --separate-files [value]", "Argument to create a single file for each component").option("-p, --path <path>", "Path to save the component files").option("-f, --file-name <fileName>", "custom name to be used in file(s) name instead of space id").description("Download your space's components schema as json").action(async (options) => {
|
|
3548
3572
|
console.log(`${chalk.blue("-")} Executing pull-components task`);
|
|
3549
3573
|
const space = program.space;
|
|
3550
|
-
const { separateFiles, path } = options;
|
|
3574
|
+
const { separateFiles, path: path2 } = options;
|
|
3551
3575
|
if (!space) {
|
|
3552
3576
|
console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID.");
|
|
3553
3577
|
process.exit(0);
|
|
@@ -3558,7 +3582,7 @@ program.command(COMMANDS.PULL_COMPONENTS).option("--sf, --separate-files [value]
|
|
|
3558
3582
|
await api.processLogin();
|
|
3559
3583
|
}
|
|
3560
3584
|
api.setSpaceId(space);
|
|
3561
|
-
await tasks.pullComponents(api, { fileName, separateFiles, path });
|
|
3585
|
+
await tasks.pullComponents(api, { fileName, separateFiles, path: path2 });
|
|
3562
3586
|
} catch (e) {
|
|
3563
3587
|
errorHandler(e, COMMANDS.PULL_COMPONENTS);
|
|
3564
3588
|
}
|
|
@@ -3653,15 +3677,26 @@ program.command(COMMANDS.SELECT).description("Usage to kickstart a boilerplate,
|
|
|
3653
3677
|
program.command(COMMANDS.SYNC).description("Sync schemas, roles, folders and stories between spaces").requiredOption(
|
|
3654
3678
|
"--type <TYPE>",
|
|
3655
3679
|
"Define what will be sync. Can be components, folders, stories, datasources or roles"
|
|
3656
|
-
).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) => {
|
|
3657
3681
|
console.log(`${chalk.blue("-")} Sync data between spaces
|
|
3658
3682
|
`);
|
|
3659
3683
|
try {
|
|
3660
3684
|
if (!api.isAuthorized()) {
|
|
3661
3685
|
await api.processLogin();
|
|
3662
3686
|
}
|
|
3663
|
-
const {
|
|
3687
|
+
const {
|
|
3688
|
+
type,
|
|
3689
|
+
target,
|
|
3690
|
+
source,
|
|
3691
|
+
startsWith,
|
|
3692
|
+
filter,
|
|
3693
|
+
keys,
|
|
3694
|
+
operations,
|
|
3695
|
+
values,
|
|
3696
|
+
componentsGroups
|
|
3697
|
+
} = options;
|
|
3664
3698
|
const _componentsGroups = componentsGroups ? componentsGroups.split(",") : null;
|
|
3699
|
+
const filterQuery = filter ? buildFilterQuery(keys, operations, values) : void 0;
|
|
3665
3700
|
const token = creds.get().token || null;
|
|
3666
3701
|
const _types = type.split(",") || [];
|
|
3667
3702
|
_types.forEach((_type) => {
|
|
@@ -3674,6 +3709,8 @@ program.command(COMMANDS.SYNC).description("Sync schemas, roles, folders and sto
|
|
|
3674
3709
|
token,
|
|
3675
3710
|
target,
|
|
3676
3711
|
source,
|
|
3712
|
+
startsWith,
|
|
3713
|
+
filterQuery,
|
|
3677
3714
|
_componentsGroups
|
|
3678
3715
|
});
|
|
3679
3716
|
console.log("\n" + chalk.green("\u2713") + " Sync data between spaces successfully completed");
|