Skip to content

Commit

Permalink
Merge pull request #90 from RKcode/feat/resolve-datasources-option-field
Browse files Browse the repository at this point in the history
`pull-components`: options to include datasource values
  • Loading branch information
alvarosabu authored Oct 15, 2024
2 parents 6e6664f + 061ad80 commit 82f4e28
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 13 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SPACE_ID> # Will save files like components-1234.json
```
Expand All @@ -106,6 +108,10 @@ $ storyblok pull-components --space <SPACE_ID> # Will save files like components
$ storyblok pull-components --space <SPACE_ID> --separate-files --prefix-presets-names --file-name production # Will save files like feature-production.json grid-production.json
```

```sh
$ storyblok pull-components --space <SPACE_ID> --resolve-datasources # Will resolve datasources for single/multiple option field
```

#### Options

* `space`: your space id
Expand Down
19 changes: 10 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,12 @@ program
.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")
.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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -299,11 +300,11 @@ program
)
.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("--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) => {
Expand All @@ -317,7 +318,7 @@ program
const {
type,
target,
source,
source,
startsWith,
filter,
keys,
Expand All @@ -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(",") || [];
Expand Down
35 changes: 32 additions & 3 deletions src/tasks/pull-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>}
*/
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) {
Expand Down
9 changes: 9 additions & 0 deletions src/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
49 changes: 49 additions & 0 deletions tests/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
}
]

Expand Down
50 changes: 49 additions & 1 deletion tests/units/pull-components.spec.js
Original file line number Diff line number Diff line change
@@ -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, _) => {
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 82f4e28

Please sign in to comment.