storyblok 3.13.1 → 3.15.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 CHANGED
@@ -28,6 +28,18 @@ Usage to kickstart a boilerplate, fieldtype or theme
28
28
  $ storyblok select
29
29
  ```
30
30
 
31
+ ### pull-languages
32
+
33
+ Download your space's languages schema as json. This command will download 1 file.
34
+
35
+ ```sh
36
+ $ storyblok pull-languages --space <SPACE_ID>
37
+ ```
38
+
39
+ #### Options
40
+
41
+ * `space`: your space id
42
+
31
43
  ### pull-components
32
44
 
33
45
  Download your space's components schema as json. This command will download 2 files: 1 for the components and 1 for the presets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyblok",
3
- "version": "3.13.1",
3
+ "version": "3.15.0",
4
4
  "description": "A simple CLI to start Storyblok from your command line.",
5
5
  "keywords": [
6
6
  "storyblok",
@@ -37,6 +37,7 @@
37
37
  "open": "^6.0.0",
38
38
  "p-series": "^2.1.0",
39
39
  "path": "^0.12.7",
40
+ "simple-uuid": "^0.0.1",
40
41
  "storyblok-js-client": "^4.5.6",
41
42
  "update-notifier": "^5.1.0",
42
43
  "xml-js": "^1.6.11"
package/src/cli.js CHANGED
@@ -69,6 +69,31 @@ program
69
69
  }
70
70
  })
71
71
 
72
+ // pull-languages
73
+ program
74
+ .command('pull-languages')
75
+ .description("Download your space's languages schema as json")
76
+ .action(async () => {
77
+ console.log(`${chalk.blue('-')} Executing pull-languages task`)
78
+ const space = program.space
79
+ if (!space) {
80
+ console.log(chalk.red('X') + ' Please provide the space as argument --space YOUR_SPACE_ID.')
81
+ process.exit(0)
82
+ }
83
+
84
+ try {
85
+ if (!api.isAuthorized()) {
86
+ await api.processLogin()
87
+ }
88
+
89
+ api.setSpaceId(space)
90
+ await tasks.pullLanguages(api, { space })
91
+ } catch (e) {
92
+ console.log(chalk.red('X') + ' An error occurred when executing the pull-languages task: ' + e.message)
93
+ process.exit(1)
94
+ }
95
+ })
96
+
72
97
  // pull-components
73
98
  program
74
99
  .command(COMMANDS.PULL_COMPONENTS)
@@ -3,6 +3,7 @@ module.exports = {
3
3
  scaffold: require('./scaffold'),
4
4
  quickstart: require('./quickstart'),
5
5
  pullComponents: require('./pull-components'),
6
+ pullLanguages: require('./pull-languages'),
6
7
  pushComponents: require('./push-components'),
7
8
  generateMigration: require('./migrations/generate'),
8
9
  runMigration: require('./migrations/run'),
@@ -0,0 +1,39 @@
1
+ const fs = require('fs')
2
+ const chalk = require('chalk')
3
+
4
+ /**
5
+ * @method pullLanguages
6
+ * @param {Object} api
7
+ * @param {Object} options { space: Number }
8
+ * @return {Promise<Object>}
9
+ */
10
+ const pullLanguages = async (api, options) => {
11
+ const { space } = options
12
+
13
+ try {
14
+ const options = await api.getSpaceOptions()
15
+ const languages = {
16
+ default_lang_name: options.default_lang_name,
17
+ languages: options.languages
18
+ }
19
+
20
+ const file = `languages.${space}.json`
21
+ const data = JSON.stringify(languages, null, 2)
22
+
23
+ console.log(`${chalk.green('✓')} We've saved your languages in the file: ${file}`)
24
+
25
+ fs.writeFile(`./${file}`, data, (err) => {
26
+ if (err) {
27
+ Promise.reject(err)
28
+ return
29
+ }
30
+
31
+ Promise.resolve(file)
32
+ })
33
+ } catch (e) {
34
+ console.error(`${chalk.red('X')} An error ocurred in pull-languages task when load components data`)
35
+ return Promise.reject(new Error(e))
36
+ }
37
+ }
38
+
39
+ module.exports = pullLanguages
@@ -1,5 +1,6 @@
1
1
  const chalk = require('chalk')
2
- const StoryblokClient = require('storyblok-js-client')
2
+ const UUID = require('simple-uuid')
3
+ const api = require('../../utils/api')
3
4
 
4
5
  class SyncDatasources {
5
6
  /**
@@ -11,9 +12,7 @@ class SyncDatasources {
11
12
  this.sourceSpaceId = options.sourceSpaceId
12
13
  this.targetSpaceId = options.targetSpaceId
13
14
  this.oauthToken = options.oauthToken
14
- this.client = new StoryblokClient({
15
- oauthToken: options.oauthToken
16
- })
15
+ this.client = api.getClient()
17
16
  }
18
17
 
19
18
  async sync () {
@@ -41,9 +40,10 @@ class SyncDatasources {
41
40
  await this.updateDatasources()
42
41
  }
43
42
 
44
- async getDatasourceEntries (spaceId, datasourceId) {
43
+ async getDatasourceEntries (spaceId, datasourceId, dimensionId = null) {
44
+ const dimensionQuery = dimensionId ? `&dimension=${dimensionId}` : ''
45
45
  try {
46
- const entriesFirstPage = await this.client.get(`spaces/${spaceId}/datasource_entries/?datasource_id=${datasourceId}`)
46
+ const entriesFirstPage = await this.client.get(`spaces/${spaceId}/datasource_entries/?datasource_id=${datasourceId}${dimensionQuery}`)
47
47
  const entriesRequets = []
48
48
  for (let i = 2; i <= Math.ceil(entriesFirstPage.total / 25); i++) {
49
49
  entriesRequets.push(this.client.get(`spaces/${spaceId}/datasource_entries/?datasource_id=${datasourceId}`, { page: i }))
@@ -88,9 +88,9 @@ class SyncDatasources {
88
88
  }
89
89
  }
90
90
 
91
- async syncDatasourceEntries (sourceId, targetId) {
91
+ async syncDatasourceEntries (datasourceId, targetId) {
92
92
  try {
93
- const sourceEntries = await this.getDatasourceEntries(this.sourceSpaceId, sourceId)
93
+ const sourceEntries = await this.getDatasourceEntries(this.sourceSpaceId, datasourceId)
94
94
  const targetEntries = await this.getDatasourceEntries(this.targetSpaceId, targetId)
95
95
  const updateEntries = targetEntries.filter(e => sourceEntries.map(se => se.name).includes(e.name))
96
96
  const addEntries = sourceEntries.filter(e => !targetEntries.map(te => te.name).includes(e.name))
@@ -126,17 +126,31 @@ class SyncDatasources {
126
126
 
127
127
  for (let i = 0; i < datasourcesToAdd.length; i++) {
128
128
  try {
129
+ console.log(` ${chalk.green('-')} Creating datasource ${datasourcesToAdd[i].name} (${datasourcesToAdd[i].slug})`)
129
130
  /* Create the datasource */
130
131
  const newDatasource = await this.client.post(`spaces/${this.targetSpaceId}/datasources`, {
131
132
  name: datasourcesToAdd[i].name,
132
133
  slug: datasourcesToAdd[i].slug
133
134
  })
134
135
 
135
- await this.syncDatasourceEntries(datasourcesToAdd[i].id, newDatasource.data.datasource.id)
136
- console.log(chalk.green('✓') + ' Created datasource ' + datasourcesToAdd[i].name)
136
+ if (datasourcesToAdd[i].dimensions.length) {
137
+ console.log(
138
+ ` ${chalk.blue('-')} Creating dimensions...`
139
+ )
140
+ const { data } = await this.createDatasourcesDimensions(datasourcesToAdd[i].dimensions, newDatasource.data.datasource)
141
+ await this.syncDatasourceEntries(datasourcesToAdd[i].id, newDatasource.data.datasource.id)
142
+ console.log(
143
+ ` ${chalk.blue('-')} Sync dimensions values...`
144
+ )
145
+ await this.syncDatasourceDimensionsValues(datasourcesToAdd[i], data.datasource)
146
+ console.log(` ${chalk.green('✓')} Created datasource ${datasourcesToAdd[i].name}`)
147
+ } else {
148
+ await this.syncDatasourceEntries(datasourcesToAdd[i].id, newDatasource.data.datasource.id)
149
+ console.log(` ${chalk.green('✓')} Created datasource ${datasourcesToAdd[i].name}`)
150
+ }
137
151
  } catch (err) {
138
152
  console.error(
139
- `${chalk.red('X')} Datasource ${datasourcesToAdd[i].name} creation failed: ${err.message}`
153
+ `${chalk.red('X')} Datasource ${datasourcesToAdd[i].name} creation failed: ${err.response.data.error || err.message}`
140
154
  )
141
155
  }
142
156
  }
@@ -154,13 +168,35 @@ class SyncDatasources {
154
168
  try {
155
169
  /* Update the datasource */
156
170
  const sourceDatasource = this.sourceDatasources.find(d => d.slug === datasourcesToUpdate[i].slug)
171
+
157
172
  await this.client.put(`spaces/${this.targetSpaceId}/datasources/${datasourcesToUpdate[i].id}`, {
158
173
  name: sourceDatasource.name,
159
174
  slug: sourceDatasource.slug
160
175
  })
161
176
 
162
- await this.syncDatasourceEntries(sourceDatasource.id, datasourcesToUpdate[i].id)
163
- console.log(chalk.green('') + ' Updated datasource ' + datasourcesToUpdate[i].name)
177
+ if (datasourcesToUpdate[i].dimensions.length) {
178
+ console.log(` ${chalk.blue('-')} Updating datasources dimensions ${datasourcesToUpdate[i].name}...`)
179
+ const sourceDimensionsNames = sourceDatasource.dimensions.map((dimension) => dimension.name)
180
+ const targetDimensionsNames = datasourcesToUpdate[i].dimensions.map((dimension) => dimension.name)
181
+ const intersection = sourceDimensionsNames.filter(item => !targetDimensionsNames.includes(item))
182
+ let datasourceToSyncDimensionsValues = datasourcesToUpdate[i]
183
+
184
+ if (intersection) {
185
+ const dimensionsToCreate = sourceDatasource.dimensions.filter((dimension) => {
186
+ if (intersection.includes(dimension.name)) return dimension
187
+ })
188
+ const { data } = await this.createDatasourcesDimensions(dimensionsToCreate, datasourcesToUpdate[i], true)
189
+ datasourceToSyncDimensionsValues = data.datasource
190
+ }
191
+
192
+ await this.syncDatasourceEntries(sourceDatasource.id, datasourcesToUpdate[i].id)
193
+
194
+ await this.syncDatasourceDimensionsValues(sourceDatasource, datasourceToSyncDimensionsValues)
195
+ console.log(`${chalk.green('✓')} Updated datasource ${datasourcesToUpdate[i].name}`)
196
+ } else {
197
+ await this.syncDatasourceEntries(sourceDatasource.id, datasourcesToUpdate[i].id)
198
+ console.log(`${chalk.green('✓')} Updated datasource ${datasourcesToUpdate[i].name}`)
199
+ }
164
200
  } catch (err) {
165
201
  console.error(
166
202
  `${chalk.red('X')} Datasource ${datasourcesToUpdate[i].name} update failed: ${err.message}`
@@ -168,6 +204,102 @@ class SyncDatasources {
168
204
  }
169
205
  }
170
206
  }
207
+
208
+ async createDatasourcesDimensions (dimensions, datasource, isToUpdate = false) {
209
+ const newDimensions = dimensions.map((dimension) => {
210
+ return {
211
+ name: dimension.name,
212
+ entry_value: dimension.entry_value,
213
+ datasource_id: datasource.id,
214
+ _uid: UUID()
215
+ }
216
+ })
217
+
218
+ let payload = null
219
+
220
+ if (isToUpdate) {
221
+ payload = {
222
+ dimensions: [...datasource.dimensions, ...newDimensions],
223
+ dimensions_attributes: [...datasource.dimensions, ...newDimensions]
224
+ }
225
+ } else {
226
+ payload = {
227
+ dimensions: newDimensions,
228
+ dimensions_attributes: newDimensions
229
+ }
230
+ }
231
+
232
+ try {
233
+ return await this.client.put(`spaces/${this.targetSpaceId}/datasources/${datasource.id}`, {
234
+ ...datasource,
235
+ ...payload
236
+ })
237
+ } catch (error) {
238
+ console.error(error)
239
+ }
240
+ }
241
+
242
+ async syncDatasourceDimensionsValues (sourceDatasource, targetDatasource) {
243
+ const sourceEntriesPromisses = []
244
+ const targetEmptyEntriesPromisses = []
245
+ try {
246
+ for (let index = 0; index < sourceDatasource.dimensions.length; index++) {
247
+ const targetDimensionId = targetDatasource.dimensions[index].id
248
+ sourceEntriesPromisses.push(...await this.getDatasourceEntries(this.sourceSpaceId, sourceDatasource.id, sourceDatasource.dimensions[index].id))
249
+ targetEmptyEntriesPromisses.push(
250
+ ...await this.getDatasourceEntries(this.targetSpaceId, targetDatasource.id, targetDimensionId).then((res) => {
251
+ return res.map((entry) => {
252
+ return {
253
+ ...entry,
254
+ target_dimension_id: targetDimensionId
255
+ }
256
+ })
257
+ })
258
+ )
259
+ }
260
+
261
+ await Promise.all(sourceEntriesPromisses)
262
+ await Promise.all(targetEmptyEntriesPromisses)
263
+
264
+ const targetEntriesPromisses = []
265
+
266
+ while (sourceEntriesPromisses.length !== 0) {
267
+ const currentSourceEntry = sourceEntriesPromisses[0]
268
+ const targetEntryIndex = targetEmptyEntriesPromisses.findIndex((tEntry) => tEntry.name === currentSourceEntry.name)
269
+ const currentTargetEntry = targetEmptyEntriesPromisses[targetEntryIndex]
270
+ const valuesAreEqual = currentTargetEntry.dimension_value === currentSourceEntry.dimension_value
271
+
272
+ if (valuesAreEqual) {
273
+ sourceEntriesPromisses.shift()
274
+ targetEmptyEntriesPromisses.splice(targetEntryIndex, 1)
275
+ } else {
276
+ const payload = {
277
+ ...currentTargetEntry,
278
+ dimension_value: currentSourceEntry.dimension_value
279
+ }
280
+
281
+ targetEntriesPromisses.push(await this.syncDimensionEntryValues(currentTargetEntry.target_dimension_id, currentTargetEntry.id, payload))
282
+ sourceEntriesPromisses.shift()
283
+ targetEmptyEntriesPromisses.splice(targetEntryIndex, 1)
284
+ }
285
+ }
286
+
287
+ await Promise.all(targetEntriesPromisses)
288
+ } catch (error) {
289
+ console.error(` ${chalk.red('X')} Sync dimensions values failed: ${error.response.data.error || error.message || error}`)
290
+ }
291
+ }
292
+
293
+ async syncDimensionEntryValues (dimensionId = null, datasourceEntryId = null, payload = null) {
294
+ try {
295
+ await this.client.put(`spaces/${this.targetSpaceId}/datasource_entries/${datasourceEntryId}`, {
296
+ datasource_entry: payload,
297
+ dimension_id: dimensionId
298
+ })
299
+ } catch (error) {
300
+ console.error(` ${chalk.red('X')} Sync entry error ${payload.name} sync failed: ${error.response.data.error || error.message}`)
301
+ }
302
+ }
171
303
  }
172
304
 
173
305
  module.exports = SyncDatasources
package/src/tasks/sync.js CHANGED
@@ -43,29 +43,29 @@ const SyncSpaces = {
43
43
 
44
44
  async syncStories () {
45
45
  console.log(chalk.green('✓') + ' Syncing stories...')
46
- var targetFolders = await this.client.getAll(`spaces/${this.targetSpaceId}/stories`, {
46
+ const targetFolders = await this.client.getAll(`spaces/${this.targetSpaceId}/stories`, {
47
47
  folder_only: 1,
48
48
  sort_by: 'slug:asc'
49
49
  })
50
50
 
51
- var folderMapping = {}
51
+ const folderMapping = {}
52
52
 
53
53
  for (let i = 0; i < targetFolders.length; i++) {
54
54
  var folder = targetFolders[i]
55
55
  folderMapping[folder.full_slug] = folder.id
56
56
  }
57
57
 
58
- var all = await this.client.getAll(`spaces/${this.sourceSpaceId}/stories`, {
58
+ const all = await this.client.getAll(`spaces/${this.sourceSpaceId}/stories`, {
59
59
  story_only: 1
60
60
  })
61
61
 
62
62
  for (let i = 0; i < all.length; i++) {
63
63
  console.log(chalk.green('✓') + ' Starting update ' + all[i].full_slug)
64
64
 
65
- var storyResult = await this.client.get('spaces/' + this.sourceSpaceId + '/stories/' + all[i].id)
66
- var sourceStory = storyResult.data.story
67
- var slugs = sourceStory.full_slug.split('/')
68
- var folderId = 0
65
+ const { data } = await this.client.get('spaces/' + this.sourceSpaceId + '/stories/' + all[i].id)
66
+ const sourceStory = data.story
67
+ const slugs = sourceStory.full_slug.split('/')
68
+ let folderId = 0
69
69
 
70
70
  if (slugs.length > 1) {
71
71
  slugs.pop()
@@ -87,7 +87,7 @@ const SyncSpaces = {
87
87
  const payload = {
88
88
  story: storyData,
89
89
  force_update: '1',
90
- ...(sourceStory.published ? { published: 1 } : {})
90
+ ...(sourceStory.published ? { publish: 1 } : {})
91
91
  }
92
92
 
93
93
  let createdStory = null
package/src/utils/api.js CHANGED
@@ -156,6 +156,15 @@ module.exports = {
156
156
  .catch(err => Promise.reject(err))
157
157
  },
158
158
 
159
+ getSpaceOptions () {
160
+ const client = this.getClient()
161
+
162
+ return client
163
+ .get(this.getPath(''))
164
+ .then((data) => data.data.space.options || {})
165
+ .catch((err) => Promise.reject(err))
166
+ },
167
+
159
168
  getComponents () {
160
169
  const client = this.getClient()
161
170
 
@@ -229,11 +229,31 @@ const FAKE_SPACES = () => [
229
229
  }
230
230
  ]
231
231
 
232
+ const FAKE_SPACE_OPTIONS = () => ({
233
+ languages: [
234
+ {
235
+ code: 'pt',
236
+ name: 'Português'
237
+ },
238
+ {
239
+ code: 'nl-be',
240
+ name: 'Dutch (Belgian)'
241
+ }
242
+ ],
243
+ hosted_backup: false,
244
+ onboarding_step: '3',
245
+ default_lang_name: 'English',
246
+ rev_share_enabled: true,
247
+ required_assest_fields: [],
248
+ use_translated_stories: false
249
+ })
250
+
232
251
  module.exports = {
233
252
  EMAIL_TEST,
234
253
  TOKEN_TEST,
235
254
  FAKE_STORIES,
236
255
  PASSWORD_TEST,
237
256
  FAKE_COMPONENTS,
238
- FAKE_SPACES
257
+ FAKE_SPACES,
258
+ FAKE_SPACE_OPTIONS
239
259
  }
@@ -0,0 +1,62 @@
1
+ const fs = require('fs')
2
+ const pullLanguages = require('../../src/tasks/pull-languages')
3
+ const { FAKE_SPACE_OPTIONS } = require('../constants')
4
+
5
+ jest.mock('fs')
6
+
7
+ describe('testing pullLanguages', () => {
8
+ afterEach(() => {
9
+ jest.clearAllMocks()
10
+ })
11
+
12
+ it('api.getSpaceOptions() should be called once time', () => {
13
+ const api = {
14
+ getSpaceOptions: jest.fn(() => Promise.resolve(FAKE_SPACE_OPTIONS()))
15
+ }
16
+
17
+ return pullLanguages(api, {})
18
+ .then(() => {
19
+ expect(api.getSpaceOptions.mock.calls.length).toBe(1)
20
+ })
21
+ })
22
+
23
+ it('api.getSpaceOptions() should be call fs.writeFile correctly', async () => {
24
+ const SPACE = 12345
25
+ const BODY = FAKE_SPACE_OPTIONS()
26
+
27
+ const api = {
28
+ getSpaceOptions () {
29
+ return Promise.resolve(BODY)
30
+ }
31
+ }
32
+
33
+ const options = {
34
+ space: SPACE
35
+ }
36
+
37
+ const expectFileName = `languages.${SPACE}.json`
38
+ const expectData = {
39
+ default_lang_name: BODY.default_lang_name,
40
+ languages: BODY.languages
41
+ }
42
+
43
+ return pullLanguages(api, options)
44
+ .then(_ => {
45
+ const [path, data] = fs.writeFile.mock.calls[0]
46
+
47
+ expect(fs.writeFile.mock.calls.length).toBe(1)
48
+ expect(path).toBe(`./${expectFileName}`)
49
+ expect(JSON.parse(data)).toEqual(expectData)
50
+ })
51
+ })
52
+
53
+ it('api.getSpaceOptions() when a error ocurred, catch the body response', async () => {
54
+ const _api = {
55
+ getSpaceOptions (_, fn) {
56
+ return Promise.reject(new Error('Failed'))
57
+ }
58
+ }
59
+
60
+ await expect(pullLanguages(_api, {})).rejects.toThrow('Error: Failed')
61
+ })
62
+ })