diff --git a/README.md b/README.md index 148a5a9..57e083c 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ $ npm i storyblok -g ## Commands - ### login Login to the Storyblok cli @@ -30,27 +29,32 @@ Login to the Storyblok cli ```sh $ storyblok login ``` + #### Login options ##### Options for Login with email and password + * `email`: your user's email address * `password`: your user's password ##### Options for Login with token (Recomended to SSO user's but works with all user accounts) + * `token`: your personal access token **Get your personal access token** + * Go to [https://app.storyblok.com/#/me/account?tab=token](https://app.storyblok.com/#/me/account?tab=token) and click on Generate new token. **For Both login options you nedd to pass the region** -* `region`: region you would like to work in. Please keep in mind that the region must match the region of your space. You can use `us`, `cn` or `eu`, if left empty, default is `eu`. This region flag will be used for the other cli's commands. +* `region`: region you would like to work in. Please keep in mind that the region must match the region of your space. You can use `us`, `cn` or `eu`, if left empty, default is `eu`. This region flag will be used for the other cli's commands. #### Login with token flag + You can also add the token directly from the login’s command, like the example below: ```sh -$ storyblok login --token --region eu +$ storyblok login --token --region eu ``` ### logout @@ -60,6 +64,7 @@ Logout from the Storyblok cli ```sh $ storyblok logout ``` + ### user Get the currently logged in user @@ -68,7 +73,6 @@ Get the currently logged in user $ storyblok user ``` - ### select Usage to kickstart a boilerplate, fieldtype or theme @@ -166,14 +170,17 @@ storyblok delete-component --space ``` #### Parameters + * `component`: The name or id of the component #### Options + * `space_id`: the space where the command should be executed. #### Examples Delete a component on your space. + ```sh storyblok delete-component 111111 --space 67819 ``` @@ -192,6 +199,7 @@ storyblok delete-components --space ``` #### Parameters + * `source`: can be a URL or path to JSON file, the path to a json file could be to a single or multiple files separated by comma, like `./pages-1234.json,../User/components/grid-1234.json` Using an **URL** @@ -213,6 +221,7 @@ $ storyblok push-components ./page.json,../grid.json,./feature.json --space 6781 ``` #### Options + * `space_id`: the space where the command should be executed. * `reverse`: When passed as an argument, deletes only those components on your space that do not appear in the JSON. * `dryrun`: when passed as an argument, does not perform any changes on the given space. @@ -220,16 +229,19 @@ $ storyblok push-components ./page.json,../grid.json,./feature.json --space 6781 #### Examples Delete all components on a certain space that occur in your local JSON. + ```sh storyblok delete-components ./components.json --space 67819 ``` Delete only those components which do not occur in your local json from your space. + ```sh storyblok delete-components ./components.json --space 67819 --reverse ``` To see the result in your console output but to not perform the command on your space, use the `--dryrun` argument. + ```sh storyblok delete-components ./components.json --space 67819 --reverse --dryrun ``` @@ -247,10 +259,9 @@ $ storyblok sync --type --source --target * `type`: describe the command type to execute. Can be: `folders`, `components`, `stories`, `datasources` or `roles`. It's possible pass multiple types separated by comma (`,`). * `source`: the source space to use to sync * `target`: the target space to use to sync -* `starts-with`: sync only stories that starts with the given string -* `filter`: sync stories based on the given filter. Required Options: Required options: `--keys`, `--operations`, `--values` -* `keys`: Multiple keys should be separated by comma. Example: `--keys key1,key2`, `--keys key1` -* `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. +* `components-groups`: Synchronize components based on their group UUIDs separated by commas. Example: `--components-groups ******-****` +* `datasources-starts-with-slug`: Synchronize datasources that starts with the given slug. Example: `--datasources-starts-with-slug global-translations` +* `datasources-starts-with-name`: Synchronize datasources that starts with the given name. Example: `--datasources-starts-with-name Translations` #### Examples @@ -261,14 +272,15 @@ $ storyblok sync --type components --source 00001 --target 00002 # Sync components and stories from `00001` space to `00002` space $ storyblok sync --type components,stories --source 00001 --target 00002 -# Sync only stories that starts with `myStartsWithString` from `00001` space to `00002` space -$ storyblok sync --type stories --source 00001 --target 00002 --starts-with myStartsWithString +# Synchronize components based on their group UUIDs separated by commas +$ storyblok sync --type components --source 00001 --target 00002 --components-groups xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# Synchronize datasources that starts with the given slug +$ storyblok sync --type datasources --source 00001 --target 00002 --datasources-starts-with-slug global-translations -# Sync only stories with a category field like `reference` from `00001` space to `00002` space -$ storyblok sync --type stories --source 00001 --target 00002 --filter --keys category --operations like --values reference +# Synchronize datasources that starts with the given name (input is converted to lowercase) +$ storyblok sync --type datasources --source 00001 --target 00002 --datasources-starts-with-name Global Translations -# Sync only stories with a category field like `reference` and a name field not like `demo` from `00001` space to `00002` space -$ storyblok sync --type stories --source 00001 --target 00002 --filter --keys category,name --operations like,not_like --values reference,demo ``` ### quickstart @@ -286,6 +298,7 @@ Create a migration file (with the name `change__.js`) inside t ```sh $ storyblok generate-migration --space --component --field ``` + It's important to note that the `component` and `field` parameters are required and must be spelled exactly as they are in Storyblok. You can check the exact name by looking at the `Block library` inside your space. #### Options @@ -338,7 +351,6 @@ $ storyblok rollback-migration --space 1234 --component Product --field title ### spaces - List all spaces of the logged account ```sh @@ -380,7 +392,7 @@ this-is-my-title;This is my title;"Lorem ipsum dolor sit amet";https://a.storybl A json file need to have following format: ```json -[ +[ { "path": "this-is-my-title", "title": "This is my title", @@ -445,10 +457,9 @@ The created file will have the following content: module.exports = function (block) { // Example to change a string to boolean // block.subtitle = !!(block.subtitle) - // Example to transfer content from other field // block.subtitle = block.other_field -} +}; ``` In the migration function you can manipulate the block variable to add or modify existing fields of the component. @@ -520,7 +531,6 @@ $ storyblok run-migration --space 00000 --component product --field image --dryr #### 2. Transform a Markdown field into a Richtext field - To transform a markdown or html field into a richtext field you first need to install a converter library. ```sh diff --git a/package.json b/package.json index 4e00f21..bf32f2e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "p-series": "^2.1.0", "path": "^0.12.7", "simple-uuid": "^0.0.1", - "storyblok-js-client": "^4.5.6", + "storyblok-js-client": "^5.12.0", "update-notifier": "^5.1.0", "xml-js": "^1.6.11" }, @@ -65,4 +65,4 @@ "master" ] } -} \ No newline at end of file +} diff --git a/src/cli.js b/src/cli.js index 5b50343..037cf56 100755 --- a/src/cli.js +++ b/src/cli.js @@ -12,7 +12,7 @@ const updateNotifier = require('update-notifier') const pkg = require('../package.json') const tasks = require('./tasks') -const { getQuestions, lastStep, api, creds, buildFilterQuery } = require('./utils') +const { getQuestions, lastStep, api, creds } = require('./utils') const { SYNC_TYPES, COMMANDS } = require('./constants') clear() @@ -24,16 +24,14 @@ console.log() // non-intrusive notify users if an update available const notifyOptions = { - isGlobal: true + isGlobal: true, } -updateNotifier({ pkg }) - .notify(notifyOptions) +updateNotifier({ pkg }).notify(notifyOptions) program.version(pkg.version) -program - .option('-s, --space [value]', 'space ID') +program.option('-s, --space [value]', 'space ID') // login program @@ -94,7 +92,7 @@ program // pull-languages program .command('pull-languages') - .description("Download your space's languages schema as json") + .description('Download your space\'s languages schema as json') .action(async () => { console.log(`${chalk.blue('-')} Executing pull-languages task`) const space = program.space @@ -122,7 +120,7 @@ program .option('--sf, --separate-files [value]', 'Argument to create a single file for each component') .option('-p, --path ', 'Path to save the component files') .option('-f, --file-name ', 'custom name to be used in file(s) name instead of space id') - .description("Download your space's components schema as json") + .description('Download your space\'s components schema as json') .action(async (options) => { console.log(`${chalk.blue('-')} Executing pull-components task`) const space = program.space @@ -150,7 +148,7 @@ program program .command(COMMANDS.PUSH_COMPONENTS + ' ') .option('-p, --presets-source ', 'Path to presets file') - .description("Download your space's components schema as json. The source parameter can be a URL to your JSON file or a path to it") + .description('Download your space\'s components schema as json. The source parameter can be a URL to your JSON file or a path to it') .action(async (source, options) => { console.log(`${chalk.blue('-')} Executing push-components task`) const space = program.space @@ -271,12 +269,9 @@ program .requiredOption('--type ', 'Define what will be sync. Can be components, folders, stories, datasources or roles') .requiredOption('--source ', 'Source space id') .requiredOption('--target ', 'Target space id') - .option('--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 ', 'Field names in your story object which should be used for filtering. Multiple keys should separated by comma.') - .option('--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 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 ', 'Synchronize components based on their group UUIDs separated by commas') + .option('--datasources-starts-with-slug ', 'Synchronize datasources that starts with the given slug') + .option('--datasources-starts-with-name ', 'Synchronize datasources that starts with the given name') .action(async (options) => { console.log(`${chalk.blue('-')} Sync data between spaces\n`) @@ -287,36 +282,34 @@ program const { type, - source, target, - startsWith, - filter, - keys, - operations, - values, - componentsGroups + source, + componentsGroups, + datasourcesStartsWithSlug, + datasourcesStartsWithName, } = options - const _componentsGroups = componentsGroups ? componentsGroups.split(',') : null + const _componentsGroups = componentsGroups + ? componentsGroups.split(',') + : null + + const token = creds.get().token || null const _types = type.split(',') || [] - _types.forEach(_type => { + _types.forEach((_type) => { if (!SYNC_TYPES.includes(_type)) { throw new Error(`The type ${_type} is not valid`) } }) - const filterQuery = filter ? buildFilterQuery(keys, operations, values) : undefined - - const token = creds.get().token || null await tasks.sync(_types, { api, token, - source, target, - startsWith, - filterQuery, - _componentsGroups + source, + _componentsGroups, + datasourcesStartsWithSlug, + datasourcesStartsWithName, }) console.log('\n' + chalk.green('✓') + ' Sync data between spaces successfully completed') @@ -399,7 +392,7 @@ program const publishOptionsAvailable = [ 'all', 'published', - 'published-with-changes' + 'published-with-changes', ] if (publish && !publishOptionsAvailable.includes(publish)) { console.log(chalk.red('X') + ' Please provide a correct publish option: all, published, or published-with-changes') @@ -449,7 +442,7 @@ program await tasks.rollbackMigration(api, field, component) } catch (e) { - console.log(chalk.red('X') + ' An error ocurred when run rollback-migration: ' + e.message) + console.log(chalk.red('X') + ' An error ocurred when run rollback-migration: ' +e.message) process.exit(1) } }) @@ -496,9 +489,7 @@ program api.setSpaceId(space) await tasks.importFiles(api, options) - console.log( - `${chalk.green('✓')} The import process was executed with success!` - ) + console.log(`${chalk.green('✓')} The import process was executed with success!`) } catch (e) { console.log(chalk.red('X') + ' An error ocurred to import data : ' + e.message) process.exit(1) @@ -511,7 +502,7 @@ if (program.rawArgs.length <= 2) { program.help() } -function errorHandler (e, command) { +function errorHandler(e, command) { if (/404/.test(e.message)) { console.log(chalk.yellow('/!\\') + ' If your space was created under US or CN region, you must provide the region us or cn upon login.') } else { diff --git a/src/constants.js b/src/constants.js index b4ef28e..4348d51 100644 --- a/src/constants.js +++ b/src/constants.js @@ -28,6 +28,11 @@ const COMMANDS = { SYNC: 'sync' } +const DEFAULT_AGENT = { + SB_Agent: 'SB-CLI', + SB_Agent_Version: process.env.npm_package_version || '3.0.0' +} + module.exports = { LOGIN_URL, SIGNUP_URL, @@ -35,5 +40,6 @@ module.exports = { SYNC_TYPES, US_API_URL, CN_API_URL, - COMMANDS + COMMANDS, + DEFAULT_AGENT } diff --git a/src/tasks/sync-commands/datasources.js b/src/tasks/sync-commands/datasources.js index e6d1e65..2e05c66 100644 --- a/src/tasks/sync-commands/datasources.js +++ b/src/tasks/sync-commands/datasources.js @@ -4,21 +4,36 @@ const api = require('../../utils/api') class SyncDatasources { /** - * @param {{ sourceSpaceId: string, targetSpaceId: string, oauthToken: string }} options + * @param {{ sourceSpaceId: string, targetSpaceId: string, oauthToken: string, datasourcesStartsWithSlug: string, datasourcesStartsWithName: string }} options */ constructor (options) { this.targetDatasources = [] this.sourceDatasources = [] + this.allSourceDatasources = [] this.sourceSpaceId = options.sourceSpaceId this.targetSpaceId = options.targetSpaceId this.oauthToken = options.oauthToken this.client = api.getClient() + this.datasourcesStartsWithSlug = options.datasourcesStartsWithSlug + this.datasourcesStartsWithName = options.datasourcesStartsWithName } async sync () { try { this.targetDatasources = await this.client.getAll(`spaces/${this.targetSpaceId}/datasources`) - this.sourceDatasources = await this.client.getAll(`spaces/${this.sourceSpaceId}/datasources`) + this.allSourceDatasources = await this.client.getAll(`spaces/${this.sourceSpaceId}/datasources`) + + if (this.datasourcesStartsWithSlug) { + this.filterAndFormattedDatasources('slug', this.datasourcesStartsWithSlug) + } + + if (this.datasourcesStartsWithName) { + this.filterAndFormattedDatasources('name', this.datasourcesStartsWithName) + } + + if (!this.datasourcesStartsWithName && !this.datasourcesStartsWithSlug) { + this.sourceDatasources = this.allSourceDatasources + } console.log( `${chalk.blue('-')} In source space #${this.sourceSpaceId}: ` @@ -31,15 +46,24 @@ class SyncDatasources { console.log(` - ${this.targetDatasources.length} datasources`) } catch (err) { console.error(`An error ocurred when loading the datasources: ${err.message}`) - + return Promise.reject(err) } - + console.log(chalk.green('-') + ' Syncing datasources...') await this.addDatasources() await this.updateDatasources() } + async filterAndFormattedDatasources(datasourceKey, startsWith) { + const filteredDatasource = this.allSourceDatasources.filter((dataSource) => dataSource[datasourceKey].toLowerCase().startsWith(startsWith.toLowerCase())) + for (const datasource of filteredDatasource) { + this.sourceDatasources.push(datasource); + } + const filteredData = this.sourceDatasources.map(obj => obj[datasourceKey]) + console.log(`${chalk.blue('-')} Source datasources where ${datasourceKey} starts with ${startsWith}: ${filteredData.join(', ')}`); + } + async getDatasourceEntries (spaceId, datasourceId, dimensionId = null) { const dimensionQuery = dimensionId ? `&dimension=${dimensionId}` : '' try { diff --git a/src/tasks/sync.js b/src/tasks/sync.js index 8c357ce..b418d8d 100644 --- a/src/tasks/sync.js +++ b/src/tasks/sync.js @@ -3,7 +3,6 @@ const chalk = require('chalk') const SyncComponents = require('./sync-commands/components') const SyncDatasources = require('./sync-commands/datasources') const { capitalize } = require('../utils') -const { startsWith } = require('lodash/string') const SyncSpaces = { targetComponents: [], @@ -15,10 +14,10 @@ const SyncSpaces = { this.sourceSpaceId = options.source this.targetSpaceId = options.target this.oauthToken = options.token - this.startsWith = options.startsWith - this.filterQuery = options.filterQuery this.client = api.getClient() - this.componentsGroups = options._componentsGroups + this.componentsGroups = options._componentsGroups, + this.datasourcesStartsWithSlug = options.datasourcesStartsWithSlug, + this.datasourcesStartsWithName = options.datasourcesStartsWithName }, async getStoryWithTranslatedSlugs (sourceStory, targetStory) { @@ -60,9 +59,7 @@ const SyncSpaces = { } const all = await this.client.getAll(`spaces/${this.sourceSpaceId}/stories`, { - story_only: 1, - ...(this.startsWith ? { starts_with: this.startsWith } : {}), - ...(this.filterQuery ? { filter_query: this.filterQuery } : {}) + story_only: 1 }) for (let i = 0; i < all.length; i++) { @@ -252,7 +249,9 @@ const SyncSpaces = { const syncDatasourcesInstance = new SyncDatasources({ sourceSpaceId: this.sourceSpaceId, targetSpaceId: this.targetSpaceId, - oauthToken: this.oauthToken + oauthToken: this.oauthToken, + datasourcesStartsWithSlug: this.datasourcesStartsWithSlug, + datasourcesStartsWithName: this.datasourcesStartsWithName }) try { diff --git a/src/utils/api.js b/src/utils/api.js index 4296b14..07762fb 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -5,7 +5,7 @@ const inquirer = require('inquirer') const creds = require('./creds') const getQuestions = require('./get-questions') -const { SIGNUP_URL, API_URL, US_API_URL, CN_API_URL } = require('../constants') +const { SIGNUP_URL, API_URL, US_API_URL, CN_API_URL, DEFAULT_AGENT } = require('../constants') module.exports = { accessToken: '', @@ -20,7 +20,10 @@ module.exports = { return new Storyblok({ accessToken: this.accessToken, oauthToken: this.oauthToken, - region: this.region + region: this.region, + headers: { + ...DEFAULT_AGENT + } }, this.apiSwitcher(region)) } catch (error) { throw new Error(error) @@ -277,7 +280,10 @@ module.exports = { const customClient = new Storyblok({ accessToken: this.accessToken, oauthToken: this.oauthToken, - region + region, + headers: { + ...DEFAULT_AGENT + } }, this.apiSwitcher(region)) return await customClient .get('spaces/', {}) diff --git a/src/utils/build-filter-query.js b/src/utils/build-filter-query.js deleted file mode 100644 index a002e8f..0000000 --- a/src/utils/build-filter-query.js +++ /dev/null @@ -1,23 +0,0 @@ -const buildFilterQuery = (keys, operations, values) => { - 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'] - if (!keys || !operations || !values) { - throw new Error('Filter options are required: --keys; --operations; --values') - } - const _keys = keys.split(',') - const _operations = operations.split(',') - const _values = values.split(',') - if (_keys.length !== _operations.length || _keys.length !== _values.length) { - throw new Error('The number of keys, operations and values must be the same') - } - const invalidOperators = _operations.filter((o) => !operators.includes(o)) - if (invalidOperators.length) { - throw new Error('Invalid operator(s) applied for filter: ' + invalidOperators.join(' ')) - } - const filterQuery = {} - _keys.forEach((key, index) => { - filterQuery[key] = { [_operations[index]]: _values[index] } - }) - return filterQuery -} - -module.exports = buildFilterQuery diff --git a/src/utils/index.js b/src/utils/index.js index 5c7499d..e92d0c4 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -6,6 +6,5 @@ module.exports = { capitalize: require('./capitalize'), findByProperty: require('./find-by-property'), parseError: require('./parse-error'), - buildFilterQuery: require('./build-filter-query'), saveFileFactory: require('./save-file-factory') -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index b63ee02..c94b536 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4359,10 +4359,10 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -storyblok-js-client@^4.5.6: - version "4.5.6" - resolved "https://registry.yarnpkg.com/storyblok-js-client/-/storyblok-js-client-4.5.6.tgz#caab0bb1ead8618f84afcc35be9305883d85e628" - integrity sha512-S0cXKkEqGDibD32K48FFodjeu/+eF++nrHhrLzo/bKxrnyQiPChPccFAnzsJCQ6uMvRuVGZzjKwM5/sMeBRthg== +storyblok-js-client@^5.12.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/storyblok-js-client/-/storyblok-js-client-5.12.0.tgz#e8100296609d3574990bdb8a7cdf3b031125e193" + integrity sha512-g096AwVtRfs+epfvuiQlI4RDTRcn+/h+WxkWQd8rm95MnRzgZMo1LJcg9NQU8Hy172dvrcjH2+DBsPxw0NAoYw== string-length@^4.0.1: version "4.0.2"