storyblok-backup 0.1.2 → 0.3.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/.env.example CHANGED
@@ -1,3 +1,3 @@
1
- STORYBLOK_OAUTH_TOKEN=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6
2
- STORYBLOK_SPACE_ID=123456
1
+ STORYBLOK_OAUTH_TOKEN=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6
2
+ STORYBLOK_SPACE_ID=123456
3
3
  STORYBLOK_REGION=eu
package/README.md CHANGED
@@ -34,7 +34,6 @@ The backup script will fetch the following resources of a Storyblok space using
34
34
  The restore script is able to individually restore the resources from the backup files (via update or create) with the following exceptions:
35
35
 
36
36
  - Assets: Only updating asset-resource-data is supported. Creating assets and updating asset-files is not supported.
37
- - Tasks: Currently not supported due to missing fields returned from management API.
38
37
  - Field types: Currently not supported
39
38
  - Workflow stage changes: No update possible.
40
39
  - Access Tokens: Creating access tokens from backup makes no sense, since it will result in a new token-string.
@@ -81,6 +80,29 @@ Call `npx storyblok-backup` with the following options:
81
80
  - 'ca': Canada
82
81
  - 'cn': China
83
82
  Alternatively, you can set the STORYBLOK_REGION environment variable.
83
+ --types <types> Comma separated list of resource-types to backup. Defaults to all.
84
+ Possible values are:
85
+ - 'stories'
86
+ - 'collaborators'
87
+ - 'components'
88
+ - 'component-groups'
89
+ - 'assets'
90
+ - 'asset-folders'
91
+ - 'internal-tags'
92
+ - 'datasources'
93
+ - 'space'
94
+ - 'space-roles'
95
+ - 'tasks'
96
+ - 'activities'
97
+ - 'presets'
98
+ - 'field-types'
99
+ - 'webhooks'
100
+ - 'workflow-stages'
101
+ - 'workflow-stage-changes'
102
+ - 'workflows'
103
+ - 'releases'
104
+ - 'pipeline-branches'
105
+ - 'access-tokens'
84
106
  --with-asset-files Downloads all files (assets) of the space. Defaults to false.
85
107
  --output-dir <dir> Directory to write the backup to. Defaults to ./.output
86
108
  (ATTENTION: Will fail if the directory already exists!)
@@ -108,7 +130,8 @@ This will create the folder `./.output/backup` and fetch all resources sorted in
108
130
  npx storyblok-backup \
109
131
  --token 1234567890abcdef \
110
132
  --space 12345 \
111
- --region ap \\
133
+ --region ap \
134
+ --types "stories,components" \
112
135
  --with-asset-files \
113
136
  --output-dir ./my-dir \
114
137
  --force \
@@ -170,6 +193,8 @@ jobs:
170
193
  with:
171
194
  name: weekly-backup
172
195
  path: .output
196
+ include-hidden-files: true
197
+ if-no-files-found: error
173
198
  ```
174
199
 
175
200
  Make sure, to set the secrets `STORYBLOK_OAUTH_TOKEN` and `STORYBLOK_SPACE_ID` in your repository settings.
@@ -214,17 +239,22 @@ Call `npx storyblok-restore` with the following options:
214
239
  - 'datasource-entries'
215
240
  - 'space'
216
241
  - 'space-role'
242
+ - 'task'
217
243
  - 'preset'
218
244
  - 'webhook'
219
245
  - 'workflow'
220
246
  - 'workflow-stage'
221
247
  - 'release'
222
248
  - 'pipeline-branch'
223
- - 'access-token
249
+ - 'access-token'
224
250
  --file <file> (required) File of resource to restore.
225
251
  --publish Perform a publish after restore of a story (default=false).
226
252
  --create Create a new resource instead of updating (default=false).
227
253
  Not supported for assets.
254
+ --propagate Propagate new story UUID to referencing stories (default=false).
255
+ Usable with create and stories. A create results in a new ID and UUID.
256
+ This option will update all stories referencing the old
257
+ UUID (as stated in the backup-json) with the new one.
228
258
  --id <file> (required if type=datasource-entries and create is set)
229
259
  ID of datasource the entries belong to.
230
260
  --verbose Will show detailed result of the restore process.
@@ -250,6 +280,7 @@ npx storyblok-restore \
250
280
  --file ./.output/backup/123456789.json \
251
281
  --publish \
252
282
  --create \
283
+ --propagate \
253
284
  --verbose
254
285
  ```
255
286
 
@@ -13,6 +13,30 @@ const startTime = performance.now()
13
13
 
14
14
  dotenvx.config({ quiet: true })
15
15
 
16
+ let resourceTypes = [
17
+ 'stories',
18
+ 'collaborators',
19
+ 'components',
20
+ 'component-groups',
21
+ 'assets',
22
+ 'asset-folders',
23
+ 'internal-tags',
24
+ 'datasources',
25
+ 'space',
26
+ 'space-roles',
27
+ 'tasks',
28
+ 'activities',
29
+ 'presets',
30
+ 'field-types',
31
+ 'webhooks',
32
+ 'workflow-stages',
33
+ 'workflow-stage-changes',
34
+ 'workflows',
35
+ 'releases',
36
+ 'pipeline-branches',
37
+ 'access-tokens',
38
+ ]
39
+
16
40
  const args = minimist(process.argv.slice(2))
17
41
 
18
42
  if ('help' in args) {
@@ -33,12 +57,15 @@ OPTIONS
33
57
  - 'ca': Canada
34
58
  - 'cn': China
35
59
  Alternatively, you can set the STORYBLOK_REGION environment variable.
36
- --with-asset-files Downloads all files (assets) of the space. Defaults to false.
37
- --output-dir <dir> Directory to write the backup to. Defaults to ./.output
60
+ --types <types> Comma separated list of resource-types to backup (default=all).
61
+ Possible values are:
62
+ - '${resourceTypes.join("'\n - '")}'
63
+ --with-asset-files Downloads all files (assets) of the space (default=false).
64
+ --output-dir <dir> Directory to write the backup to (default=./.output)
38
65
  (ATTENTION: Will fail if the directory already exists!)
39
66
  --force Force deletion and recreation of existing output directory.
40
- --create-zip Create a zip file of the backup. Defaults to false.
41
- --zip-prefix <dir> Prefix for the zip file. Defaults to 'backup'.
67
+ --create-zip Create a zip file of the backup (default=false).
68
+ --zip-prefix <dir> Prefix for the zip file. (default='backup').
42
69
  (The suffix will automatically be the current date.)
43
70
  --verbose Will show detailed output for every file written.
44
71
  --help Show this help
@@ -51,6 +78,7 @@ MAXIMAL EXAMPLE
51
78
  --token 1234567890abcdef \\
52
79
  --space 12345 \\
53
80
  --region ap \\
81
+ --types "stories,components" \\
54
82
  --with-asset-files \\
55
83
  --output-dir ./my-dir \\
56
84
  --force \\
@@ -87,6 +115,19 @@ if ('region' in args || process.env.STORYBLOK_REGION) {
87
115
  }
88
116
  }
89
117
 
118
+ if ('types' in args) {
119
+ const typesToBackup = args.types.split(',')
120
+ for (const type of typesToBackup) {
121
+ if (!resourceTypes.includes(type)) {
122
+ console.log(
123
+ `Error: Invalid type parameter stated. "${type}" is not a valid resource type. Use --help to find out more.`
124
+ )
125
+ process.exit(1)
126
+ }
127
+ }
128
+ resourceTypes = typesToBackup
129
+ }
130
+
90
131
  const verbose = 'verbose' in args
91
132
 
92
133
  const outputDir = args['output-dir'] || './.output'
@@ -132,51 +173,30 @@ if (fs.existsSync(outputDir)) {
132
173
  fs.rmSync(outputDir, { recursive: true, force: true })
133
174
  }
134
175
 
135
- // Create output directories
176
+ // Create output directories (except for space, since this json will be saved in the root)
136
177
  fs.mkdirSync(backupDir, { recursive: true })
137
-
138
- const resources = [
139
- 'stories',
140
- 'collaborators',
141
- 'components',
142
- 'component-groups',
143
- 'assets',
144
- 'asset-folders',
145
- 'internal-tags',
146
- 'datasources',
147
- 'space-roles',
148
- 'tasks',
149
- 'activities',
150
- 'presets',
151
- 'field-types',
152
- 'webhooks',
153
- 'workflow-stages',
154
- 'workflow-stage-changes',
155
- 'workflows',
156
- 'releases',
157
- 'pipeline-branches',
158
- 'access-tokens',
159
- ]
160
- resources.forEach((resource) => fs.mkdirSync(`${backupDir}/${resource}`))
178
+ resourceTypes.forEach(
179
+ (resource) => resource === 'space' || fs.mkdirSync(`${backupDir}/${resource}`)
180
+ )
161
181
 
162
182
  // Function to perform a default fetch
163
- const defaultFetch = async (type, folder, fileField, fileFieldObject) => {
164
- await StoryblokMAPI.getAll(`spaces/${spaceId}/${type}`)
165
- .then((items) => {
166
- if (type === 'datasources') {
167
- console.log(items)
168
- }
169
- items.forEach((item) =>
170
- writeJson(
171
- folder,
172
- fileFieldObject ? item[fileFieldObject][fileField] : item[fileField],
173
- item
183
+ const defaultFetch = async (endpoint, type, fileField, fileFieldObject) => {
184
+ if (resourceTypes.includes(type)) {
185
+ console.log(`Fetching ${type}`)
186
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/${endpoint}`)
187
+ .then((items) => {
188
+ items.forEach((item) =>
189
+ writeJson(
190
+ type,
191
+ fileFieldObject ? item[fileFieldObject][fileField] : item[fileField],
192
+ item
193
+ )
174
194
  )
175
- )
176
- })
177
- .catch((error) => {
178
- throw error
179
- })
195
+ })
196
+ .catch((error) => {
197
+ throw error
198
+ })
199
+ }
180
200
  }
181
201
 
182
202
  // Function to write a file
@@ -204,138 +224,129 @@ const downloadFile = async (type, name, url) => {
204
224
  }
205
225
 
206
226
  // Fetch space info
207
- console.log(`Fetching space`)
208
- await StoryblokMAPI.get(`spaces/${spaceId}/`)
209
- .then((space) => {
210
- writeJson(null, `space-${spaceId}`, space.data.space)
211
- })
212
- .catch((error) => {
213
- throw error
214
- })
227
+ if (resourceTypes.includes('space')) {
228
+ console.log(`Fetching space`)
229
+ await StoryblokMAPI.get(`spaces/${spaceId}/`)
230
+ .then((space) => {
231
+ writeJson(null, `space-${spaceId}`, space.data.space)
232
+ })
233
+ .catch((error) => {
234
+ throw error
235
+ })
236
+ }
215
237
 
216
238
  // Fetch all stories
217
- console.log(`Fetching stories`)
218
- await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
219
- .then(async (stories) => {
220
- for (const story of stories) {
221
- await StoryblokMAPI.get(`spaces/${spaceId}/stories/${story.id}`)
222
- .then((response) => {
223
- delete response.data.story.preview_token
224
- writeJson('stories', story.id, response.data.story)
225
- })
226
- .catch((error) => {
227
- throw error
228
- })
229
- }
230
- })
231
- .catch((error) => {
232
- throw error
233
- })
239
+ if (resourceTypes.includes('stories')) {
240
+ console.log(`Fetching stories`)
241
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
242
+ .then(async (stories) => {
243
+ for (const story of stories) {
244
+ await StoryblokMAPI.get(`spaces/${spaceId}/stories/${story.id}`)
245
+ .then((response) => {
246
+ delete response.data.story.preview_token
247
+ writeJson('stories', story.id, response.data.story)
248
+ })
249
+ .catch((error) => {
250
+ throw error
251
+ })
252
+ }
253
+ })
254
+ .catch((error) => {
255
+ throw error
256
+ })
257
+ }
234
258
 
235
259
  // Fetch all collaborators
236
- console.log(`Fetching collaborators`)
237
260
  await defaultFetch('collaborators', 'collaborators', 'user_id')
238
261
 
239
262
  // Fetch all components
240
- console.log(`Fetching components`)
241
263
  await defaultFetch('components', 'components', 'name')
242
264
 
243
265
  // Fetch all component-groups
244
- console.log(`Fetching component-groups`)
245
266
  await defaultFetch('component_groups', 'component-groups', 'id')
246
267
 
247
268
  // Fetch all assets (including files)
248
- console.log(`Fetching assets`)
249
- await StoryblokMAPI.getAll(`spaces/${spaceId}/assets`)
250
- .then(async (assets) => {
251
- for (const asset of assets) {
252
- writeJson('assets', asset.id, asset)
253
- if ('with-asset-files' in args) {
254
- const fileExtension = asset.filename.split('.').at(-1)
255
- const fileName = asset.id + '.' + fileExtension
256
- await downloadFile('assets', fileName, asset.filename)
269
+ if (resourceTypes.includes('assets')) {
270
+ console.log(`Fetching assets`)
271
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/assets`)
272
+ .then(async (assets) => {
273
+ for (const asset of assets) {
274
+ writeJson('assets', asset.id, asset)
275
+ if ('with-asset-files' in args) {
276
+ const fileExtension = asset.filename.split('.').at(-1)
277
+ const fileName = asset.id + '.' + fileExtension
278
+ await downloadFile('assets', fileName, asset.filename)
279
+ }
257
280
  }
258
- }
259
- })
260
- .catch((error) => {
261
- throw error
262
- })
281
+ })
282
+ .catch((error) => {
283
+ throw error
284
+ })
285
+ }
263
286
 
264
287
  // Fetch all asset-folders
265
- console.log(`Fetching asset-folders`)
266
288
  await defaultFetch('asset_folders', 'asset-folders', 'id')
267
289
 
268
290
  // Fetch all internal-tags
269
- console.log(`Fetching internal-tags`)
270
291
  await defaultFetch('internal_tags', 'internal-tags', 'id')
271
292
 
272
293
  // Fetch all datasources (including entries)
273
- console.log(`Fetching datasources`)
274
- await StoryblokMAPI.getAll(`spaces/${spaceId}/datasources`)
275
- .then(async (datasources) => {
276
- for (const datasource of datasources) {
277
- writeJson('datasources', datasource.id, datasource)
278
- await StoryblokMAPI.getAll(`spaces/${spaceId}/datasource_entries`, {
279
- datasource_id: datasource.id,
280
- })
281
- .then((dateSourceEntries) =>
282
- writeJson('datasources', datasource.id + '_entries', dateSourceEntries)
283
- )
284
- .catch((error) => {
285
- throw error
294
+ if (resourceTypes.includes('datasources')) {
295
+ console.log(`Fetching datasources`)
296
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/datasources`)
297
+ .then(async (datasources) => {
298
+ for (const datasource of datasources) {
299
+ writeJson('datasources', datasource.id, datasource)
300
+ await StoryblokMAPI.getAll(`spaces/${spaceId}/datasource_entries`, {
301
+ datasource_id: datasource.id,
286
302
  })
287
- }
288
- })
289
- .catch((error) => {
290
- throw error
291
- })
303
+ .then((dateSourceEntries) =>
304
+ writeJson('datasources', datasource.id + '_entries', dateSourceEntries)
305
+ )
306
+ .catch((error) => {
307
+ throw error
308
+ })
309
+ }
310
+ })
311
+ .catch((error) => {
312
+ throw error
313
+ })
314
+ }
292
315
 
293
316
  // Fetch all space roles
294
- console.log(`Fetching space roles`)
295
317
  await defaultFetch('space_roles', 'space-roles', 'id')
296
318
 
297
319
  // Fetch all tasks
298
- console.log(`Fetching tasks`)
299
320
  await defaultFetch('tasks', 'tasks', 'id')
300
321
 
301
322
  // Fetch all activities
302
- console.log(`Fetching activities`)
303
323
  await defaultFetch('activities', 'activities', 'id', 'activity')
304
324
 
305
325
  // Fetch all presets
306
- console.log(`Fetching presets`)
307
326
  await defaultFetch('presets', 'presets', 'id')
308
327
 
309
328
  // Fetch all field-types
310
- console.log(`Fetching field-types`)
311
329
  await defaultFetch('field_types', 'field-types', 'name')
312
330
 
313
331
  // Fetch all webhooks
314
- console.log(`Fetching webhooks`)
315
332
  await defaultFetch('webhook_endpoints', 'webhooks', 'id')
316
333
 
317
334
  // Fetch all workflow-stages
318
- console.log(`Fetching workflow-stages`)
319
335
  await defaultFetch('workflow_stages', 'workflow-stages', 'id')
320
336
 
321
337
  // Fetch all workflow-stage-changes
322
- console.log(`Fetching workflow-stage-changes`)
323
338
  await defaultFetch('workflow_stage_changes', 'workflow-stage-changes', 'id')
324
339
 
325
340
  // Fetch all workflows
326
- console.log(`Fetching workflows`)
327
341
  await defaultFetch('workflows', 'workflows', 'id')
328
342
 
329
343
  // Fetch all releases
330
- console.log(`Fetching releases`)
331
344
  await defaultFetch('releases', 'releases', 'id')
332
345
 
333
346
  // Fetch all pipeline branches
334
- console.log(`Fetching pipeline branches`)
335
347
  await defaultFetch('branches', 'pipeline-branches', 'id')
336
348
 
337
349
  // Fetch all access tokens
338
- console.log(`Fetching access tokens`)
339
350
  await defaultFetch('api_keys', 'access-tokens', 'id')
340
351
 
341
352
  // Create zip file
@@ -22,7 +22,7 @@ const resourceTypes = [
22
22
  'datasource-entries',
23
23
  'space',
24
24
  'space-role',
25
- //'task', // Currently not supported due to missing fields returned from management API
25
+ 'task',
26
26
  'preset',
27
27
  // 'field-type',
28
28
  'webhook',
@@ -54,11 +54,15 @@ OPTIONS
54
54
  - 'cn': China
55
55
  Alternatively, you can set the STORYBLOK_REGION environment variable.
56
56
  --type <type> (required) Type of resource to restore. Possible values are:
57
- - '${resourceTypes.join("'\n - '")}
57
+ - '${resourceTypes.join("'\n - '")}'
58
58
  --file <file> (required) File of resource to restore.
59
59
  --publish Perform a publish after restore of a story (default=false).
60
60
  --create Create a new resource instead of updating (default=false).
61
61
  Not supported for assets.
62
+ --propagate Propagate new story UUID to referencing stories (default=false).
63
+ Usable with create and stories. A create results in a new ID and UUID.
64
+ This option will update all stories referencing the old
65
+ UUID (as stated in the backup-json) with the new one.
62
66
  --id <file> (required if type=datasource-entries and create is set)
63
67
  ID of datasource the entries belong to.
64
68
  --verbose Will show detailed result of the restore process.
@@ -76,6 +80,7 @@ MAXIMAL EXAMPLE
76
80
  --file ./.output/backup/123456789.json \\
77
81
  --publish \\
78
82
  --create \\
83
+ --propagate \\
79
84
  --verbose
80
85
  `)
81
86
  process.exit(0)
@@ -137,6 +142,18 @@ const publish = 'publish' in args
137
142
 
138
143
  const create = 'create' in args
139
144
 
145
+ const propagate = 'propagate' in args
146
+ if (propagate && !create) {
147
+ console.log('Error: Propagate is only usable with create. Use --help to find out more.')
148
+ process.exit(1)
149
+ }
150
+ if (propagate && args.type !== 'story') {
151
+ console.log(
152
+ 'Error: Propagate is only usable with story resources. Use --help to find out more.'
153
+ )
154
+ process.exit(1)
155
+ }
156
+
140
157
  // Init Management API
141
158
  const StoryblokMAPI = new StoryblokClient({
142
159
  oauthToken: oauthToken,
@@ -144,7 +161,7 @@ const StoryblokMAPI = new StoryblokClient({
144
161
  })
145
162
 
146
163
  // Function to perform a default single resource restore
147
- const defaultSingleRestore = async (type, id, params) => {
164
+ const defaultSingleRestore = async (type, id, params, forceUpdate) => {
148
165
  if (publish) {
149
166
  params.publish = 1
150
167
  }
@@ -158,14 +175,15 @@ const defaultSingleRestore = async (type, id, params) => {
158
175
  url = `${url}/${type}`
159
176
  }
160
177
 
161
- if (create) {
162
- await StoryblokMAPI.post(url, params)
178
+ if (create && !forceUpdate) {
179
+ return await StoryblokMAPI.post(url, params)
163
180
  .then((response) => {
164
181
  console.log(`Created "${type}" resource.`)
165
182
  if (verbose) {
166
183
  console.log('Result:')
167
184
  console.log(response.data)
168
185
  }
186
+ return response.data
169
187
  })
170
188
  .catch((error) => {
171
189
  throw error
@@ -174,13 +192,14 @@ const defaultSingleRestore = async (type, id, params) => {
174
192
  if (id) {
175
193
  url = `${url}/${id}`
176
194
  }
177
- await StoryblokMAPI.put(url, params)
195
+ return await StoryblokMAPI.put(url, params)
178
196
  .then((response) => {
179
197
  console.log(`Updated "${type}" resource with id "${id}".`)
180
198
  if (verbose) {
181
199
  console.log('Result:')
182
200
  console.log(response.data)
183
201
  }
202
+ return response.data
184
203
  })
185
204
  .catch((error) => {
186
205
  throw error
@@ -193,7 +212,35 @@ const resource = JSON.parse(fs.readFileSync(args.file, 'utf8'))
193
212
  switch (args.type) {
194
213
  case 'story':
195
214
  delete resource.updated_at
196
- await defaultSingleRestore('stories', resource.id, { story: resource })
215
+ const result = await defaultSingleRestore('stories', resource.id, { story: resource })
216
+ if (create && propagate) {
217
+ const oldUuid = resource.uuid
218
+ const newUuid = result.story.uuid
219
+ const newId = result.story.id
220
+ console.log(`Propagating UUID change from "${oldUuid}" to "${newUuid}":`)
221
+ const referencingStories = await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`, {
222
+ reference_search: oldUuid,
223
+ excluding_ids: [newId],
224
+ })
225
+ for (const referencingStory of referencingStories) {
226
+ const fullReferencingStoryResult = await StoryblokMAPI.get(
227
+ `spaces/${spaceId}/stories/${referencingStory.id}`
228
+ )
229
+ await defaultSingleRestore(
230
+ 'stories',
231
+ referencingStory.id,
232
+ {
233
+ story: JSON.parse(
234
+ JSON.stringify(fullReferencingStoryResult.data.story).replaceAll(
235
+ oldUuid,
236
+ newUuid
237
+ )
238
+ ),
239
+ },
240
+ true
241
+ )
242
+ }
243
+ }
197
244
  break
198
245
  case 'collaborator':
199
246
  await defaultSingleRestore(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyblok-backup",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "npx CLI tool to create a full backup of a Storyblok space and restore single resources from it.",
5
5
  "scripts": {
6
6
  "upgrade": "npx npm-check-updates -i -u && pnpm install",