diff --git a/README.md b/README.md index b0ca332..9ab7248 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ Download your space's components schema as json. By default this command will do It's highly recommended to use also the `--prefix-presets-names` or `-ppn` parameter if you use `--separate-files` because it will prefix the names of the individual files with the name of the component. This feature solves the issue of multiple presets from different components but with the same name, being written in the same file. In a future major version this will become the default behavior. +If you want to resolve datasources for single/multiple option field from your Storyblok components, you can use `--resolve-datasources` or `--rd`, it will fill up the options fields with the datasource's options. + ```sh $ storyblok pull-components --space # Will save files like components-1234.json ``` @@ -106,6 +108,10 @@ $ storyblok pull-components --space # Will save files like components $ storyblok pull-components --space --separate-files --prefix-presets-names --file-name production # Will save files like feature-production.json grid-production.json ``` +```sh +$ storyblok pull-components --space --resolve-datasources # Will resolve datasources for single/multiple option field +``` + #### Options * `space`: your space id diff --git a/src/cli.ts b/src/cli.ts index e26dc9b..33bbcce 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -143,11 +143,12 @@ program .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") .option("-ppn, --prefix-presets-names", "Prefixes the names of presets with the name of the components") + .option("--rd, --resolve-datasources", "Fill options for single/multiple option field with the linked datasource") .description("Download your space's components schema as json") .action(async (options) => { console.log(`${chalk.blue("-")} Executing pull-components task`); const space = program.space; - const { separateFiles, path, prefixPresetsNames } = options; + const { separateFiles, path, prefixPresetsNames, resolveDatasources } = options; if (!space) { console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); process.exit(0); @@ -161,7 +162,7 @@ program } api.setSpaceId(space); - await tasks.pullComponents(api, { fileName, separateFiles, path, prefixPresetsNames }); + await tasks.pullComponents(api, { fileName, separateFiles, path, prefixPresetsNames, resolveDatasources }); } catch (e) { errorHandler(e, COMMANDS.PULL_COMPONENTS); } @@ -299,11 +300,11 @@ program ) .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("--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("--components-full-sync", "Synchronize components by overriding any property from source to target") .action(async (options) => { @@ -317,7 +318,7 @@ program const { type, target, - source, + source, startsWith, filter, keys, @@ -329,7 +330,7 @@ program const _componentsGroups = componentsGroups ? componentsGroups.split(",") : null; const _componentsFullSync = !!componentsFullSync; - const filterQuery = filter ? buildFilterQuery(keys, operations, values) : undefined + const filterQuery = filter ? buildFilterQuery(keys, operations, values) : undefined; const token = creds.get().token || null; const _types = type.split(",") || []; diff --git a/src/tasks/pull-components.js b/src/tasks/pull-components.js index f0e85ac..e3b402e 100644 --- a/src/tasks/pull-components.js +++ b/src/tasks/pull-components.js @@ -17,22 +17,51 @@ const getNameFromComponentGroups = (groups, uuid) => { return '' } +const resolveDatasourceOptions = async (api, components) => { + const datasources = await api.getDatasources() + + for (const datasource of datasources) { + const datasourceEntries = await api.getDatasourceEntries(datasource.id) + datasource.entries = datasourceEntries + } + + return components.map(component => { + const schema = component.schema + + for (const field in schema) { + if (schema[field].source === 'internal' && schema[field].datasource_slug) { + const datasource = datasources.find(ds => ds.slug === schema[field].datasource_slug) + + if (datasource) { + schema[field].options = datasource.entries.map(entry => ({ value: entry.value, name: entry.name })) + } + } + } + + return component + }) +} + /** * @method pullComponents * @param {Object} api - * @param {Object} options { fileName: string, separateFiles: Boolean, path: String } + * @param {Object} options { fileName: string, separateFiles: Boolean, path: String, resolveDatasources: Boolean } * @return {Promise} */ const pullComponents = async (api, options) => { - const { fileName, separateFiles, path, prefixPresetsNames } = options + const { fileName, separateFiles, path, prefixPresetsNames, resolveDatasources } = options try { const componentGroups = await api.getComponentGroups() - const components = await api.getComponents() + let components = await api.getComponents() const presets = await api.getPresets() + if (resolveDatasources) { + components = await resolveDatasourceOptions(api, components) + } + components.forEach(component => { const groupUuid = component.component_group_uuid if (groupUuid) { diff --git a/src/utils/api.js b/src/utils/api.js index b382b56..cae2ec4 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -268,6 +268,15 @@ export default { .catch(err => Promise.reject(err)) }, + getDatasourceEntries (id) { + const client = this.getClient() + + return client + .get(this.getPath(`datasource_entries?datasource_id=${id}`)) + .then(data => data.data.datasource_entries || []) + .catch(err => Promise.reject(err)) + }, + deleteDatasource (id) { const client = this.getClient() diff --git a/tests/constants.js b/tests/constants.js index b377385..349d23f 100644 --- a/tests/constants.js +++ b/tests/constants.js @@ -145,6 +145,55 @@ export const FAKE_COMPONENTS = () => [ preset_id: null, real_name: 'hero', component_group_uuid: null + }, + { + name: 'meta', + display_name: null, + created_at: '2019-11-06T17:07:04.196Z', + updated_at: '2019-11-06T18:12:29.136Z', + id: 4, + schema: { + robot: { + type: "option", + source: "internal", + datasource_slug: "robots", + } + }, + image: null, + preview_field: null, + is_root: false, + preview_tmpl: null, + is_nestable: true, + all_presets: [], + preset_id: null, + real_name: 'meta', + component_group_uuid: null + }, +] + +export const FAKE_DATASOURCES = () => [ + { + id: 1, + name: "Robots", + slug: "robots", + dimensions: [], + created_at: "2019-10-15T17:00:32.212Z", + updated_at: "2019-11-15T17:00:32.212Z", + }, +] + +export const FAKE_DATASOURCE_ENTRIES = () => [ + { + id: 1, + name: "No index", + value: "noindex", + dimension_value: "" + }, + { + id: 2, + name: "No follow", + value: "nofollow", + dimension_value: "" } ] diff --git a/tests/units/pull-components.spec.js b/tests/units/pull-components.spec.js index 4320ad0..75cccc2 100644 --- a/tests/units/pull-components.spec.js +++ b/tests/units/pull-components.spec.js @@ -1,6 +1,6 @@ import fs from 'fs' import pullComponents from '../../src/tasks/pull-components' -import { FAKE_PRESET, FAKE_COMPONENTS } from '../constants' +import { FAKE_PRESET, FAKE_COMPONENTS, FAKE_DATASOURCES, FAKE_DATASOURCE_ENTRIES } from '../constants' import { jest } from '@jest/globals' jest.spyOn(fs, 'writeFileSync').mockImplementation(jest.fn((key, data, _) => { @@ -126,6 +126,54 @@ describe('testing pullComponents', () => { } }) + it('pull components should be call fs.writeFile correctly and return filled options from datasource entries', async () => { + const SPACE = 12345 + + const api = { + getComponents () { + return Promise.resolve([FAKE_COMPONENTS()[5]]) + }, + getComponentGroups () { + return Promise.resolve([]) + }, + getDatasources () { + return Promise.resolve(FAKE_DATASOURCES()) + }, + getDatasourceEntries () { + return Promise.resolve(FAKE_DATASOURCE_ENTRIES()) + }, + getPresets () { + return Promise.resolve([]) + } + } + + const options = { + fileName: SPACE, + resolveDatasources: true + } + + const expectFileName = `components.${SPACE}.json` + + await pullComponents(api, options) + const [path, data] = fs.writeFile.mock.calls[0] + + expect(fs.writeFile.mock.calls.length).toBe(1) + expect(path).toBe(`./${expectFileName}`) + expect(JSON.parse(data)).toEqual({ + components: [{ + ...FAKE_COMPONENTS()[5], + schema: { + robot: { + type: "option", + source: "internal", + datasource_slug: "robots", + options: FAKE_DATASOURCE_ENTRIES().map(entry => ({ value: entry.value, name: entry.name })) + } + } + }] + }) + }) + it('api.getComponents() when a error ocurred, catch the body response', async () => { const _api = { getComponents (_, fn) {