storyblok-backup 0.0.4 → 0.1.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 ADDED
@@ -0,0 +1,3 @@
1
+ STORYBLOK_OAUTH_TOKEN=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6
2
+ STORYBLOK_SPACE_ID=123456
3
+ STORYBLOK_REGION=eu
package/README.md CHANGED
@@ -5,13 +5,17 @@
5
5
 
6
6
  A npx CLI tool to create a full backup of a space of the [Storyblok CMS](https://www.storyblok.com).
7
7
 
8
- The script will fetch the following resources of a Storyblok space using the Management API and archive them in a zip file:
8
+ A restore tool to restore (create or update) resources is also included.
9
+
10
+ The backup script will fetch the following resources of a Storyblok space using the Management API and archive them in a zip file:
9
11
 
10
12
  - Stories
13
+ - Collaborators
11
14
  - Components
12
15
  - Component groups
13
16
  - Assets (optionally incl. original files)
14
17
  - Asset folders
18
+ - Internal Tags
15
19
  - Datasources (incl. entries)
16
20
  - Space
17
21
  - Space Roles
@@ -19,17 +23,31 @@ The script will fetch the following resources of a Storyblok space using the Man
19
23
  - Activities
20
24
  - Presets
21
25
  - Field types
26
+ - Webhooks
27
+ - Workflows
22
28
  - Workflow stages
23
29
  - Workflow stage changes
24
- - Custom workflows
25
30
  - Releases
31
+ - Pipeline Branches
32
+ - Access Tokens
33
+
34
+ The restore script is able to individually restore the the resources from the backup files (via update or create) with the following exceptions:
35
+
36
+ - Assets: Creating assets is not supported
37
+ - Tasks: Currently not supported due to missing fields returned from management API
38
+ - Field types: Currently not supported
39
+ - Workflow stage changes: No update possible
40
+ - Access Tokens: Creating access tokens from backup makes no sense, since it will result in a new token-string.
26
41
 
27
42
  ## Installation
28
43
 
29
44
  ```shell
30
45
 
31
46
  # simply auto-download and run via npx
47
+ ## for backup:
32
48
  $ npx storyblok-backup
49
+ ## for restore:
50
+ $ npx -p storyblok-backup storyblok-restore
33
51
 
34
52
  # or install globally
35
53
  $ npm install -g storyblok-backup
@@ -46,21 +64,26 @@ $ pnpm add storyblok-backup
46
64
 
47
65
  ## Usage
48
66
 
67
+ ### Backup
68
+
49
69
  Call `npx storyblok-backup` with the following options:
50
70
 
51
- ### Options
71
+ #### Backup options
52
72
 
53
73
  ```text
54
74
  --token <token> (required) Personal OAuth access token created
55
75
  in the account settings of a Stoyblok user.
56
76
  (NOT the Access Token of a Space!)
77
+ Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
57
78
  --space <space_id> (required) ID of the space to backup
79
+ Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
58
80
  --region <region> Region of the space. Possible values are:
59
81
  - 'eu' (default): EU
60
82
  - 'us': US
61
83
  - 'ap': Australia
62
84
  - 'ca': Canada
63
85
  - 'cn': China
86
+ Alternatively, you can set the STORYBLOK_REGION environment variable.
64
87
  --with-asset-files Downloads all files (assets) of the space. Defaults to false.
65
88
  --output-dir <dir> Directory to write the backup to. Defaults to ./.output
66
89
  (ATTENTION: Will fail if the directory already exists!)
@@ -72,7 +95,9 @@ Call `npx storyblok-backup` with the following options:
72
95
  --help Show this help
73
96
  ```
74
97
 
75
- ### Minimal example
98
+ OAuth token, space-id and region can be set via environment variables. You can also use a `.env` file in your project root for this (see `.env.example`).
99
+
100
+ #### Minimal backup example
76
101
 
77
102
  ```shell
78
103
  npx storyblok-backup --token 1234567890abcdef --space 12345
@@ -80,7 +105,7 @@ npx storyblok-backup --token 1234567890abcdef --space 12345
80
105
 
81
106
  This will create the folder `./.output/backup` and fetch all resources sorted into folders.
82
107
 
83
- ### Maximal example
108
+ #### Maximal backup example
84
109
 
85
110
  ```shell
86
111
  npx storyblok-backup \
@@ -97,7 +122,7 @@ npx storyblok-backup \
97
122
 
98
123
  This will create the folder `./my-dir/backup`, fetch all resources (incl. the original file assets) sorted into folders, zip them to `./my-dir/daily-Y-m-d-H-i-s.zip`, and log every written file to console.
99
124
 
100
- ## Continuous Integration
125
+ #### Continuous backup integration
101
126
 
102
127
  You can e.g. use this script to create periodic backups of Storyblok spaces using GitHub Actions and artifacts.
103
128
 
@@ -158,6 +183,79 @@ If you create multiple workflows for daily, weekly and monthly backups, by chang
158
183
 
159
184
  Also keep in mind, that there is a limit on artifact storage and runner minutes ([see GitHub docs](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes)).
160
185
 
186
+ ### Restore
187
+
188
+ Call `npx -p storyblok-backup storyblok-restore` with the following options:
189
+
190
+ #### Restore options
191
+
192
+ ```text
193
+ --token <token> (required) Personal OAuth access token created
194
+ in the account settings of a Stoyblok user.
195
+ (NOT the Access Token of a Space!)
196
+ Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
197
+ --space <space_id> (required) ID of the space to backup
198
+ Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
199
+ --region <region> Region of the space. Possible values are:
200
+ - 'eu' (default): EU
201
+ - 'us': US
202
+ - 'ap': Australia
203
+ - 'ca': Canada
204
+ - 'cn': China
205
+ Alternatively, you can set the STORYBLOK_REGION environment variable.
206
+ --type <type> (required) Type of resource to restore. Possible values are:
207
+ - 'story'
208
+ - 'collaborator'
209
+ - 'component'
210
+ - 'component-group'
211
+ - 'asset'
212
+ - 'asset-folder'
213
+ - 'internal-tag'
214
+ - 'datasource'
215
+ - 'datasource-entries'
216
+ - 'space'
217
+ - 'space-role'
218
+ - 'preset'
219
+ - 'webhook'
220
+ - 'workflow'
221
+ - 'workflow-stage'
222
+ - 'release'
223
+ - 'pipeline-branch'
224
+ - 'access-token
225
+ --file <file> (required) File of resource to restore.
226
+ --publish Perform a publish after restore of a story (default=false).
227
+ --create Create a new resource instead of updating (default=false).
228
+ Not supported for assets.
229
+ --id <file> (required if type=datasource-entries and create is set)
230
+ ID of datasource the entries belong to.
231
+ --verbose Will show detailed result of the restore process.
232
+ --help Show this help
233
+ ```
234
+
235
+ #### Minimal restore example
236
+
237
+ ```shell
238
+ npx -p storyblok-backup storyblok-restore --token 1234567890abcdef --space 12345 --type story --file ./.output/backup/123456789.json
239
+ ```
240
+
241
+ This will restore the story from the stated file by updating it.
242
+
243
+ #### Maximal restore example
244
+
245
+ ```shell
246
+ npx -p storyblok-backup storyblok-restore \
247
+ --token 1234567890abcdef \
248
+ --space 12345 \
249
+ --region ap \
250
+ --type story \
251
+ --file ./.output/backup/123456789.json \
252
+ --publish \
253
+ --create \
254
+ --verbose
255
+ ```
256
+
257
+ This will restore the story by creating a new story, immediately publish it, and log the API result to console.
258
+
161
259
  ## License
162
260
 
163
261
  This package is open-sourced software licensed under the [MIT license](https://github.com/webflorist/storyblok-backup/blob/main/LICENSE.).
@@ -7,9 +7,12 @@ import zipLib from 'zip-lib'
7
7
  import minimist from 'minimist'
8
8
  import StoryblokClient from 'storyblok-js-client'
9
9
  import { performance } from 'perf_hooks'
10
+ import dotenvx from '@dotenvx/dotenvx'
10
11
 
11
12
  const startTime = performance.now()
12
13
 
14
+ dotenvx.config({ quiet: true })
15
+
13
16
  const args = minimist(process.argv.slice(2))
14
17
 
15
18
  if ('help' in args) {
@@ -20,13 +23,16 @@ OPTIONS
20
23
  --token <token> (required) Personal OAuth access token created
21
24
  in the account settings of a Stoyblok user.
22
25
  (NOT the Access Token of a Space!)
26
+ Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
23
27
  --space <space_id> (required) ID of the space to backup
28
+ Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
24
29
  --region <region> Region of the space. Possible values are:
25
30
  - 'eu' (default): EU
26
31
  - 'us': US
27
32
  - 'ap': Australia
28
33
  - 'ca': Canada
29
34
  - 'cn': China
35
+ Alternatively, you can set the STORYBLOK_REGION environment variable.
30
36
  --with-asset-files Downloads all files (assets) of the space. Defaults to false.
31
37
  --output-dir <dir> Directory to write the backup to. Defaults to ./.output
32
38
  (ATTENTION: Will fail if the directory already exists!)
@@ -55,21 +61,30 @@ MAXIMAL EXAMPLE
55
61
  process.exit(0)
56
62
  }
57
63
 
58
- if (!('token' in args)) {
64
+ if (!('token' in args) && !process.env.STORYBLOK_OAUTH_TOKEN) {
59
65
  console.log(
60
- 'Error: State your oauth token via the --token argument. Use --help to find out more.'
66
+ 'Error: State your oauth token via the --token argument or the environment variable STORYBLOK_OAUTH_TOKEN. Use --help to find out more.'
61
67
  )
62
68
  process.exit(1)
63
69
  }
70
+ const oauthToken = args.token || process.env.STORYBLOK_OAUTH_TOKEN
64
71
 
65
- if (!('space' in args)) {
66
- console.log('Error: State your space id via the --space argument. Use --help to find out more.')
72
+ if (!('space' in args) && !process.env.STORYBLOK_SPACE_ID) {
73
+ console.log(
74
+ 'Error: State your space id via the --space argument or the environment variable STORYBLOK_SPACE_ID. Use --help to find out more.'
75
+ )
67
76
  process.exit(1)
68
77
  }
78
+ const spaceId = args.space || process.env.STORYBLOK_SPACE_ID
69
79
 
70
- if ('region' in args && !['eu', 'us', 'ap', 'ca', 'cn'].includes(args.region)) {
71
- console.log('Error: Invalid region parameter stated. Use --help to find out more.')
72
- process.exit(1)
80
+ let region = 'eu'
81
+ if ('region' in args || process.env.STORYBLOK_REGION) {
82
+ region = args.region || process.env.STORYBLOK_REGION
83
+
84
+ if (!['eu', 'us', 'ap', 'ca', 'cn'].includes(region)) {
85
+ console.log('Error: Invalid region parameter stated. Use --help to find out more.')
86
+ process.exit(1)
87
+ }
73
88
  }
74
89
 
75
90
  const verbose = 'verbose' in args
@@ -85,10 +100,6 @@ if (fs.existsSync(outputDir) && !('force' in args)) {
85
100
  process.exit(1)
86
101
  }
87
102
 
88
- const spaceId = args.space
89
-
90
- const region = args['region'] || 'eu'
91
-
92
103
  const filePrefix = args['zip-prefix'] || 'backup'
93
104
 
94
105
  const fileName =
@@ -112,7 +123,7 @@ if ('create-zip' in args) {
112
123
 
113
124
  // Init Management API
114
125
  const StoryblokMAPI = new StoryblokClient({
115
- oauthToken: args.token,
126
+ oauthToken: oauthToken,
116
127
  region: region,
117
128
  })
118
129
 
@@ -126,20 +137,25 @@ fs.mkdirSync(backupDir, { recursive: true })
126
137
 
127
138
  const resources = [
128
139
  'stories',
140
+ 'collaborators',
129
141
  'components',
130
142
  'component-groups',
131
143
  'assets',
132
144
  'asset-folders',
145
+ 'internal-tags',
133
146
  'datasources',
134
147
  'space-roles',
135
148
  'tasks',
136
149
  'activities',
137
150
  'presets',
138
151
  'field-types',
152
+ 'webhooks',
139
153
  'workflow-stages',
140
154
  'workflow-stage-changes',
141
155
  'workflows',
142
156
  'releases',
157
+ 'pipeline-branches',
158
+ 'access-tokens',
143
159
  ]
144
160
  resources.forEach((resource) => fs.mkdirSync(`${backupDir}/${resource}`))
145
161
 
@@ -170,7 +186,7 @@ const writeJson = (folder, file, content) => {
170
186
  outputFile += `/${folder}`
171
187
  }
172
188
  outputFile += `/${file}.json`
173
- fs.writeFile(outputFile, JSON.stringify(content), (error) => {
189
+ fs.writeFileSync(outputFile, JSON.stringify(content, null, 2), (error) => {
174
190
  if (error) {
175
191
  throw error
176
192
  }
@@ -203,7 +219,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
203
219
  .then(async (stories) => {
204
220
  for (const story of stories) {
205
221
  await StoryblokMAPI.get(`spaces/${spaceId}/stories/${story.id}`)
206
- .then((response) => writeJson('stories', story.id, response.data.story))
222
+ .then((response) => {
223
+ delete response.data.story.preview_token
224
+ writeJson('stories', story.id, response.data.story)
225
+ })
207
226
  .catch((error) => {
208
227
  throw error
209
228
  })
@@ -213,6 +232,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/stories`)
213
232
  throw error
214
233
  })
215
234
 
235
+ // Fetch all collaborators
236
+ console.log(`Fetching collaborators`)
237
+ await defaultFetch('collaborators', 'collaborators', 'user_id')
238
+
216
239
  // Fetch all components
217
240
  console.log(`Fetching components`)
218
241
  await defaultFetch('components', 'components', 'name')
@@ -242,6 +265,10 @@ await StoryblokMAPI.getAll(`spaces/${spaceId}/assets`)
242
265
  console.log(`Fetching asset-folders`)
243
266
  await defaultFetch('asset_folders', 'asset-folders', 'id')
244
267
 
268
+ // Fetch all internal-tags
269
+ console.log(`Fetching internal-tags`)
270
+ await defaultFetch('internal_tags', 'internal-tags', 'id')
271
+
245
272
  // Fetch all datasources (including entries)
246
273
  console.log(`Fetching datasources`)
247
274
  await StoryblokMAPI.getAll(`spaces/${spaceId}/datasources`)
@@ -283,6 +310,10 @@ await defaultFetch('presets', 'presets', 'id')
283
310
  console.log(`Fetching field-types`)
284
311
  await defaultFetch('field_types', 'field-types', 'name')
285
312
 
313
+ // Fetch all webhooks
314
+ console.log(`Fetching webhooks`)
315
+ await defaultFetch('webhook_endpoints', 'webhooks', 'id')
316
+
286
317
  // Fetch all workflow-stages
287
318
  console.log(`Fetching workflow-stages`)
288
319
  await defaultFetch('workflow_stages', 'workflow-stages', 'id')
@@ -299,6 +330,14 @@ await defaultFetch('workflows', 'workflows', 'id')
299
330
  console.log(`Fetching releases`)
300
331
  await defaultFetch('releases', 'releases', 'id')
301
332
 
333
+ // Fetch all pipeline branches
334
+ console.log(`Fetching pipeline branches`)
335
+ await defaultFetch('branches', 'pipeline-branches', 'id')
336
+
337
+ // Fetch all access tokens
338
+ console.log(`Fetching access tokens`)
339
+ await defaultFetch('api_keys', 'access-tokens', 'id')
340
+
302
341
  // Create zip file
303
342
  if ('create-zip' in args) {
304
343
  console.log(`Creating zip file`)
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ import fs from 'fs'
4
+ import minimist from 'minimist'
5
+ import StoryblokClient from 'storyblok-js-client'
6
+ import { performance } from 'perf_hooks'
7
+ import dotenvx from '@dotenvx/dotenvx'
8
+
9
+ const startTime = performance.now()
10
+
11
+ dotenvx.config({ quiet: true })
12
+
13
+ const resourceTypes = [
14
+ 'story',
15
+ 'collaborator',
16
+ 'component',
17
+ 'component-group',
18
+ 'asset',
19
+ 'asset-folder',
20
+ 'internal-tag',
21
+ 'datasource',
22
+ 'datasource-entries',
23
+ 'space',
24
+ 'space-role',
25
+ //'task', // Currently not supported due to missing fields returned from management API
26
+ 'preset',
27
+ // 'field-type',
28
+ 'webhook',
29
+ 'workflow',
30
+ 'workflow-stage',
31
+ 'release',
32
+ 'pipeline-branch',
33
+ 'access-token',
34
+ ]
35
+
36
+ const args = minimist(process.argv.slice(2))
37
+
38
+ if ('help' in args) {
39
+ console.log(`USAGE
40
+ $ npx -p storyblok-backup storyblok-restore
41
+
42
+ OPTIONS
43
+ --token <token> (required) Personal OAuth access token created
44
+ in the account settings of a Stoyblok user.
45
+ (NOT the Access Token of a Space!)
46
+ Alternatively, you can set the STORYBLOK_OAUTH_TOKEN environment variable.
47
+ --space <space_id> (required) ID of the space to backup
48
+ Alternatively, you can set the STORYBLOK_SPACE_ID environment variable.
49
+ --region <region> Region of the space. Possible values are:
50
+ - 'eu' (default): EU
51
+ - 'us': US
52
+ - 'ap': Australia
53
+ - 'ca': Canada
54
+ - 'cn': China
55
+ Alternatively, you can set the STORYBLOK_REGION environment variable.
56
+ --type <type> (required) Type of resource to restore. Possible values are:
57
+ - '${resourceTypes.join("'\n - '")}
58
+ --file <file> (required) File of resource to restore.
59
+ --publish Perform a publish after restore of a story (default=false).
60
+ --create Create a new resource instead of updating (default=false).
61
+ Not supported for assets.
62
+ --id <file> (required if type=datasource-entries and create is set)
63
+ ID of datasource the entries belong to.
64
+ --verbose Will show detailed result of the restore process.
65
+ --help Show this help
66
+
67
+ MINIMAL EXAMPLE
68
+ $ npx -p storyblok-backup storyblok-restore --token 1234567890abcdef --space 12345 --type story --file ./.output/backup/123456789.json
69
+
70
+ MAXIMAL EXAMPLE
71
+ $ npx -p storyblok-backup storyblok-restore \\
72
+ --token 1234567890abcdef \\
73
+ --space 12345 \\
74
+ --region ap \\
75
+ --type story \\
76
+ --file ./.output/backup/123456789.json \\
77
+ --publish \\
78
+ --create \\
79
+ --verbose
80
+ `)
81
+ process.exit(0)
82
+ }
83
+
84
+ if (!('token' in args) && !process.env.STORYBLOK_OAUTH_TOKEN) {
85
+ console.log(
86
+ 'Error: State your oauth token via the --token argument or the environment variable STORYBLOK_OAUTH_TOKEN. Use --help to find out more.'
87
+ )
88
+ process.exit(1)
89
+ }
90
+ const oauthToken = args.token || process.env.STORYBLOK_OAUTH_TOKEN
91
+
92
+ if (!('space' in args) && !process.env.STORYBLOK_SPACE_ID) {
93
+ console.log(
94
+ 'Error: State your space id via the --space argument or the environment variable STORYBLOK_SPACE_ID. Use --help to find out more.'
95
+ )
96
+ process.exit(1)
97
+ }
98
+ const spaceId = args.space || process.env.STORYBLOK_SPACE_ID
99
+
100
+ let region = 'eu'
101
+ if ('region' in args || process.env.STORYBLOK_REGION) {
102
+ region = args.region || process.env.STORYBLOK_REGION
103
+
104
+ if (!['eu', 'us', 'ap', 'ca', 'cn'].includes(region)) {
105
+ console.log('Error: Invalid region parameter stated. Use --help to find out more.')
106
+ process.exit(1)
107
+ }
108
+ }
109
+
110
+ if (!('type' in args)) {
111
+ console.log(
112
+ 'Error: State the resource type to restore via the --type argument. Use --help to find out more.'
113
+ )
114
+ process.exit(1)
115
+ }
116
+
117
+ if (!resourceTypes.includes(args.type)) {
118
+ console.log(`Error: Invalid resource type "${args.type}". Use --help to find out more.`)
119
+ process.exit(1)
120
+ }
121
+
122
+ if (!('file' in args)) {
123
+ console.log(
124
+ 'Error: State the resource file to restore via the --file argument. Use --help to find out more.'
125
+ )
126
+ process.exit(1)
127
+ }
128
+
129
+ if (!fs.existsSync(args.file)) {
130
+ console.log(`Error: Stated file "${args.file}" does not exist.`)
131
+ process.exit(1)
132
+ }
133
+
134
+ const verbose = 'verbose' in args
135
+
136
+ const publish = 'publish' in args
137
+
138
+ const create = 'create' in args
139
+
140
+ // Init Management API
141
+ const StoryblokMAPI = new StoryblokClient({
142
+ oauthToken: oauthToken,
143
+ region: region,
144
+ })
145
+
146
+ // Function to perform a default single resource restore
147
+ const defaultSingleRestore = async (type, id, params) => {
148
+ if (publish) {
149
+ params.publish = 1
150
+ }
151
+ let url = `spaces`
152
+
153
+ if (!(create && args.type === 'space')) {
154
+ url = `${url}/${spaceId}`
155
+ }
156
+
157
+ if (type) {
158
+ url = `${url}/${type}`
159
+ }
160
+
161
+ if (create) {
162
+ await StoryblokMAPI.post(url, params)
163
+ .then((response) => {
164
+ console.log(`Created "${type}" resource.`)
165
+ if (verbose) {
166
+ console.log('Result:')
167
+ console.log(response.data)
168
+ }
169
+ })
170
+ .catch((error) => {
171
+ throw error
172
+ })
173
+ } else {
174
+ if (id) {
175
+ url = `${url}/${id}`
176
+ }
177
+ await StoryblokMAPI.put(url, params)
178
+ .then((response) => {
179
+ console.log(`Updated "${type}" resource with id "${id}".`)
180
+ if (verbose) {
181
+ console.log('Result:')
182
+ console.log(response.data)
183
+ }
184
+ })
185
+ .catch((error) => {
186
+ throw error
187
+ })
188
+ }
189
+ }
190
+
191
+ const resource = JSON.parse(fs.readFileSync(args.file, 'utf8'))
192
+
193
+ switch (args.type) {
194
+ case 'story':
195
+ delete resource.updated_at
196
+ await defaultSingleRestore('stories', resource.id, { story: resource })
197
+ break
198
+ case 'collaborator':
199
+ await defaultSingleRestore(
200
+ 'collaborators',
201
+ resource.id,
202
+ create
203
+ ? {
204
+ email: resource.user.userid,
205
+ role: resource.role,
206
+ space_id: resource.space_id,
207
+ permissions: resource.permissions,
208
+ space_role_ids: resource.space_role_ids,
209
+ allow_multiple_roles_creation: resource.role === 'multi',
210
+ }
211
+ : { collaborator: resource }
212
+ )
213
+ break
214
+ case 'component':
215
+ await defaultSingleRestore('components', resource.id, { component: resource })
216
+ break
217
+ case 'component-group':
218
+ await defaultSingleRestore('component_groups', resource.id, { component_group: resource })
219
+ break
220
+ case 'asset':
221
+ if (create) {
222
+ console.log('Error: Creating assets is not supported.')
223
+ process.exit(1)
224
+ }
225
+ await defaultSingleRestore('assets', resource.id, { asset: resource })
226
+ break
227
+ case 'asset-folder':
228
+ await defaultSingleRestore('asset_folders', resource.id, { asset_folder: resource })
229
+ break
230
+ case 'internal-tag':
231
+ await defaultSingleRestore('internal_tags', resource.id, { internal_tag: resource })
232
+ break
233
+ case 'datasource':
234
+ await defaultSingleRestore('datasources', resource.id, { datasource: resource })
235
+ break
236
+ case 'datasource-entries':
237
+ if (create && !('id' in args)) {
238
+ console.log(
239
+ 'Error: State the datasource ID via the --id argument. Use --help to find out more.'
240
+ )
241
+ process.exit(1)
242
+ }
243
+ for (const entry of resource) {
244
+ if (create) {
245
+ entry.datasource_id = args.id
246
+ }
247
+ await defaultSingleRestore('datasource_entries', entry.id, {
248
+ datasource_entry: entry,
249
+ })
250
+ }
251
+ break
252
+ case 'space':
253
+ await defaultSingleRestore(null, null, { space: resource })
254
+ break
255
+ case 'space-role':
256
+ await defaultSingleRestore('space_roles', resource.id, { space_role: resource })
257
+ break
258
+ case 'task':
259
+ await defaultSingleRestore('tasks', resource.id, { task: resource })
260
+ break
261
+ case 'preset':
262
+ await defaultSingleRestore('presets', resource.id, { preset: resource })
263
+ break
264
+ case 'webhook':
265
+ await defaultSingleRestore('webhook_endpoints', resource.id, { webhook_endpoint: resource })
266
+ break
267
+ case 'workflow':
268
+ await defaultSingleRestore('workflows', resource.id, { workflow: resource })
269
+ break
270
+ case 'workflow-stage':
271
+ await defaultSingleRestore('workflow_stages', resource.id, { workflow_stage: resource })
272
+ break
273
+ case 'release':
274
+ await defaultSingleRestore('releases', resource.id, { release: resource })
275
+ break
276
+ case 'pipeline-branch':
277
+ await defaultSingleRestore('branches', resource.id, { branch: resource })
278
+ break
279
+ case 'access-token':
280
+ if (create) {
281
+ console.log(
282
+ 'Error: Creating access-tokens from backup is not possible, since it will result in a new token.'
283
+ )
284
+ process.exit(1)
285
+ }
286
+ await defaultSingleRestore('api_keys', resource.id, { api_key: resource })
287
+ break
288
+ }
289
+
290
+ const endTime = performance.now()
291
+
292
+ console.log(`Restore successful in ${Math.round((endTime - startTime) / 1000)} seconds.`)
293
+ process.exit(0)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "storyblok-backup",
3
- "version": "0.0.4",
4
- "description": "npx CLI tool to create a full backup of a Storyblok space",
3
+ "version": "0.1.0",
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",
7
7
  "lint:js": "eslint --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .lintignore .",
@@ -12,7 +12,8 @@
12
12
  "lintfix": "pnpm lintfix:js && pnpm lintfix:prettier"
13
13
  },
14
14
  "bin": {
15
- "storyblok-backup": "bin/storyblok-backup.mjs"
15
+ "storyblok-backup": "bin/storyblok-backup.mjs",
16
+ "storyblok-restore": "bin/storyblok-restore.mjs"
16
17
  },
17
18
  "repository": {
18
19
  "type": "git",
@@ -40,6 +41,7 @@
40
41
  "prettier": "^3.3.2"
41
42
  },
42
43
  "dependencies": {
44
+ "@dotenvx/dotenvx": "^1.6.2",
43
45
  "minimist": "^1.2.8",
44
46
  "storyblok-js-client": "^6.7.3",
45
47
  "zip-lib": "^1.0.4"