Skip to content
This repository was archived by the owner on Jan 19, 2026. It is now read-only.

Commit 0bee124

Browse files
feat: Merge pull request #343 from storyblok/feature/migration-fields
Feature Migration Fields
2 parents d59ecf4 + 2f0671b commit 0bee124

19 files changed

Lines changed: 3269 additions & 1907 deletions

README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,35 @@ Login to the Storyblok cli
116116
$ storyblok login
117117
```
118118

119+
### generate-migration
120+
121+
Create a migration file (with the name `change_<COMPONENT>_<FIELD>.js`) inside the `migrations` folder. Check **Migrations** section to more details
122+
123+
```sh
124+
$ storyblok generate-migration --space <SPACE_ID> --component <COMPONENT_NAME> --field <FIELD>
125+
```
126+
127+
#### Options
128+
129+
* `space`: space where the component is
130+
* `component`: component name. It needs to be a valid component
131+
* `field`: name of field
132+
133+
### run-migration
134+
135+
Execute a specific migration file. Check **Migrations** section to more details
136+
137+
```sh
138+
$ storyblok run-migration --space <SPACE_ID> --component <COMPONENT_NAME> --field <FIELD> --dryrun
139+
```
140+
141+
#### Options
142+
143+
* `space`: space where the component is
144+
* `component`: component name. It needs to be a valid component
145+
* `field`: name of field
146+
* `dryrun`: when passed as an argument, does not perform the migration
147+
119148
### Help
120149

121150
For global help
@@ -138,6 +167,74 @@ For view the CLI version
138167
$ storyblok -V # or --version
139168
```
140169

170+
## Migrations
171+
172+
Migrations are a convenient way to update stories in Storyblok. This section shows how to create a migration file and execute it using the CLI.
173+
174+
### Creating a migration file
175+
176+
To create a migration file, you need to execute the `generate-migration` command:
177+
178+
```sh
179+
# creating a migration file to product component to update the price
180+
$ storyblok generate-migration --space 00000 --component product --field price
181+
```
182+
183+
When you run this command, a folder called `migrations` will be created in the location where you ran the command (if this folder does not exist) and a file called `change_product_price.js` will be created inside it.
184+
185+
The created file will have the following content:
186+
187+
```js
188+
// here, 'subtitle' is the name of the field defined when you execute the generate command
189+
module.exports = function (block) {
190+
// Example to change a string to boolean
191+
// block.subtitle = !!(block.subtitle)
192+
193+
// Example to transfer content from other field
194+
// block.subtitle = block.other_field
195+
}
196+
```
197+
198+
As you can see, this file takes two parameters:
199+
200+
* `block`: the component content from the story
201+
* `field`: the field content from this component
202+
203+
Inside the migration function, you can manipulate the blok whatever you want, because the blok content will be used to update the story. This will be occurr recursively for all content in the story, so, this change will be affect the entirely content.
204+
205+
### Running the migration file
206+
207+
To run the migration function, you need to execute the `run-migration` command, as the following:
208+
209+
```sh
210+
# you can use the --dryrun option to don't execute, only show the component updates
211+
$ storyblok run-migration --space 00000 --component product --field price
212+
```
213+
214+
### Example
215+
216+
Let's create an example to update all occurrences of the image field in product component. Let's replace the url from `//a.storyblok.com` to `//my-custom-domain.com`.
217+
218+
First, you need to create the migration function:
219+
220+
```sh
221+
$ storyblok generate-migration --space 00000 --component product --field image
222+
```
223+
224+
After, let's update the default file:
225+
226+
```js
227+
module.exports = function (block) {
228+
block.image = block.image.replace('a.storyblok.com', 'my-custom-domain.com')
229+
}
230+
```
231+
232+
Lastly, let's execute the migration file:
233+
234+
```sh
235+
$ storyblok run-migration --space 00000 --component product --field image
236+
```
237+
141238
## You're looking for a headstart?
142239

143240
Check out our guides for client side apps (VueJS, Angular, React, ...), static site (Jekyll, NuxtJs, ...), dynamic site examples (Node, PHP, Python, Laravel, ...) on our [Getting Started](https://www.storyblok.com/getting-started) page.

__mocks__/fs-extra.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const fs = jest.genMockFromModule('fs-extra')
2+
3+
let mockFiles = Object.create(null)
4+
5+
// used by pull-components.spec.js
6+
const pathExists = jest.fn((key) => {
7+
return !!mockFiles[key]
8+
})
9+
10+
const outputFile = jest.fn((path, data, fn) => {
11+
mockFiles[path] = data
12+
return Promise.resolve(true)
13+
})
14+
15+
const __clearMockFiles = () => {
16+
mockFiles = Object.create(null)
17+
}
18+
19+
const __setMockFiles = (mock) => {
20+
mockFiles = mock
21+
}
22+
23+
fs.pathExists = pathExists
24+
25+
fs.outputFile = outputFile
26+
27+
fs.__clearMockFiles = __clearMockFiles
28+
29+
fs.__setMockFiles = __setMockFiles
30+
31+
module.exports = fs

package.json

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,34 +21,35 @@
2121
"author": "Dominik Angerer <dominikangerer1@gmail.com>, Alexander Feiglstorfer <delooks@gmail.com>",
2222
"license": "MIT",
2323
"dependencies": {
24-
"axios": "^0.19.0",
25-
"chalk": "^1.1.3",
26-
"clear": "0.0.1",
27-
"commander": "^4.0.0",
28-
"figlet": "^1.2.0",
24+
"axios": "^0.19.2",
25+
"chalk": "^4.1.0",
26+
"clear": "0.1.0",
27+
"commander": "^5.1.0",
28+
"figlet": "^1.4.0",
29+
"fs-extra": "^9.0.1",
2930
"github-download": "^0.5.0",
30-
"inquirer": "^3.0.1",
31-
"minimist": "^1.2.0",
31+
"inquirer": "^7.2.0",
32+
"lodash": "^4.17.15",
3233
"netrc": "0.1.4",
33-
"opn": "^5.1.0",
34+
"on-change": "^2.0.1",
35+
"opn": "^6.0.0",
3436
"p-series": "^2.1.0",
3537
"path": "^0.12.7",
36-
"storyblok-js-client": "^2.0.10",
37-
"unirest": "^0.5.1",
38-
"update-notifier": "^4.0.0"
38+
"storyblok-js-client": "^2.5.0",
39+
"update-notifier": "^4.1.0"
3940
},
4041
"engines": {
41-
"node": ">=9.11.0"
42+
"node": ">=10.0.0"
4243
},
4344
"devDependencies": {
4445
"concat-stream": "^2.0.0",
45-
"eslint": "^6.6.0",
46-
"eslint-config-standard": "^14.1.0",
47-
"eslint-plugin-import": "^2.18.2",
48-
"eslint-plugin-jest": "^23.0.2",
49-
"eslint-plugin-node": "^10.0.0",
46+
"eslint": "^7.2.0",
47+
"eslint-config-standard": "^14.1.1",
48+
"eslint-plugin-import": "^2.21.2",
49+
"eslint-plugin-jest": "^23.13.2",
50+
"eslint-plugin-node": "^11.1.0",
5051
"eslint-plugin-promise": "^4.2.1",
5152
"eslint-plugin-standard": "^4.0.1",
52-
"jest": "^24.9.0"
53+
"jest": "^26.0.1"
5354
}
5455
}

src/cli.js

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,10 @@ program
4646
}
4747

4848
try {
49-
const questions = getQuestions('login', {}, api)
50-
const { email, password } = await inquirer.prompt(questions)
51-
52-
await api.login(email, password)
53-
console.log(chalk.green('✓') + ' Log in successfully! Token has been added to .netrc file.')
49+
await api.processLogin()
5450
process.exit(0)
5551
} catch (e) {
56-
console.log(chalk.red('X') + ' An error ocurred when login the user')
57-
console.error(e)
52+
console.log(chalk.red('X') + ' An error occurred when logging the user: ' + e.message)
5853
process.exit(1)
5954
}
6055
})
@@ -69,8 +64,8 @@ program
6964
console.log('Logged out successfully! Token has been removed from .netrc file.')
7065
process.exit(0)
7166
} catch (e) {
72-
console.log(chalk.red('X') + ' An error ocurred when logout the user')
73-
console.error(e)
67+
console.log(chalk.red('X') + ' An error occurred when logging out the user: ' + e.message)
68+
process.exit(1)
7469
}
7570
})
7671

@@ -79,24 +74,23 @@ program
7974
.command('pull-components')
8075
.description("Download your space's components schema as json")
8176
.action(async () => {
82-
console.log(`${chalk.blue('-')} Executing push-components task`)
77+
console.log(`${chalk.blue('-')} Executing pull-components task`)
8378
const space = program.space
8479
if (!space) {
8580
console.log(chalk.red('X') + ' Please provide the space as argument --space YOUR_SPACE_ID.')
8681
process.exit(0)
8782
}
8883

8984
try {
90-
const questions = await getQuestions('pull-components', { space }, api)
91-
92-
await inquirer.prompt(questions)
85+
if (!api.isAuthorized()) {
86+
await api.processLogin()
87+
}
9388

9489
api.setSpaceId(space)
9590
await tasks.pullComponents(api, { space })
9691
} catch (e) {
97-
console.log(chalk.red('X') + ' An error ocurred when execute the pull-components task')
98-
console.error(e)
99-
process.exit(0)
92+
console.log(chalk.red('X') + ' An error occurred when executing the pull-components task: ' + e.message)
93+
process.exit(1)
10094
}
10195
})
10296

@@ -114,16 +108,15 @@ program
114108
}
115109

116110
try {
117-
const questions = await getQuestions('push-components', { space }, api)
118-
119-
await inquirer.prompt(questions)
111+
if (!api.isAuthorized()) {
112+
await api.processLogin()
113+
}
120114

121115
api.setSpaceId(space)
122116
await tasks.pushComponents(api, { source })
123117
} catch (e) {
124-
console.log(chalk.red('X') + ' An error ocurred when execute the push-components task')
125-
console.error(e)
126-
process.exit(0)
118+
console.log(chalk.red('X') + ' An error occurred when executing the push-components task: ' + e.message)
119+
process.exit(1)
127120
}
128121
})
129122

@@ -145,8 +138,8 @@ program
145138
console.log(chalk.green('✓') + ' - source/scss/components/below/_' + name + '.scss')
146139
process.exit(0)
147140
} catch (e) {
148-
console.log(chalk.red('X') + ' An error ocurred execute operations to create the component')
149-
console.error(e)
141+
console.log(chalk.red('X') + ' An error occurred when executing operations to create the component: ' + e.message)
142+
process.exit(1)
150143
}
151144
})
152145

@@ -163,8 +156,8 @@ program
163156

164157
await lastStep(answers)
165158
} catch (e) {
166-
console.error(e)
167-
process.exit(0)
159+
console.error(chalk.red('X') + ' An error ocurred when execute the select command: ' + e.message)
160+
process.exit(1)
168161
}
169162
})
170163

@@ -194,11 +187,7 @@ program
194187
})
195188

196189
if (!api.isAuthorized()) {
197-
const questions = getQuestions('login', {}, api)
198-
const { email, password } = await inquirer.prompt(questions)
199-
200-
await api.login(email, password)
201-
console.log(chalk.green('✓') + ' Log in successfully! Token has been added to .netrc file.')
190+
await api.processLogin()
202191
}
203192

204193
const token = creds.get().token || null
@@ -211,9 +200,8 @@ program
211200

212201
console.log('\n' + chalk.green('✓') + ' Sync data between spaces successfully completed')
213202
} catch (e) {
214-
console.error(chalk.red('X') + ' An error ocurred when sync spaces')
215-
console.error(e)
216-
process.exit(0)
203+
console.error(chalk.red('X') + ' An error ocurred when syncing spaces: ' + e.message)
204+
process.exit(1)
217205
}
218206
})
219207

@@ -228,8 +216,70 @@ program
228216
const answers = await inquirer.prompt(questions)
229217
await tasks.quickstart(api, answers, space)
230218
} catch (e) {
231-
console.log(chalk.red('X') + ' An error ocurred when execute quickstart operations')
232-
console.error(e)
219+
console.log(chalk.red('X') + ' An error ocurred when execute quickstart operations: ' + e.message)
220+
process.exit(1)
221+
}
222+
})
223+
224+
program
225+
.command('generate-migration')
226+
.description('Generate a content migration file')
227+
.requiredOption('-c, --component <COMPONENT_NAME>', 'Name of the component')
228+
.requiredOption('-f, --field <FIELD_NAME>', 'Name of the component field')
229+
.action(async (options) => {
230+
const field = options.field || ''
231+
const component = options.component || ''
232+
233+
const space = program.space
234+
if (!space) {
235+
console.log(chalk.red('X') + ' Please provide the space as argument --space YOUR_SPACE_ID.')
236+
process.exit(1)
237+
}
238+
239+
console.log(`${chalk.blue('-')} Creating the migration file in ./migrations/change_${component}_${field}.js\n`)
240+
241+
try {
242+
if (!api.isAuthorized()) {
243+
await api.processLogin()
244+
}
245+
246+
api.setSpaceId(space)
247+
await tasks.generateMigration(api, component, field)
248+
} catch (e) {
249+
console.log(chalk.red('X') + ' An error ocurred when generate the migration file: ' + e.message)
250+
process.exit(1)
251+
}
252+
})
253+
254+
program
255+
.command('run-migration')
256+
.description('Run a migration file')
257+
.requiredOption('-c, --component <COMPONENT_NAME>', 'Name of the component')
258+
.requiredOption('-f, --field <FIELD_NAME>', 'Name of the component field')
259+
.option('--dryrun', 'Do not update the story content')
260+
.action(async (options) => {
261+
const field = options.field || ''
262+
const component = options.component || ''
263+
const isDryrun = !!options.dryrun
264+
265+
const space = program.space
266+
if (!space) {
267+
console.log(chalk.red('X') + ' Please provide the space as argument --space YOUR_SPACE_ID.')
268+
process.exit(1)
269+
}
270+
271+
console.log(`${chalk.blue('-')} Processing the migration ./migrations/change_${component}_${field}.js\n`)
272+
273+
try {
274+
if (!api.isAuthorized()) {
275+
await api.processLogin()
276+
}
277+
278+
api.setSpaceId(space)
279+
await tasks.runMigration(api, component, field, { isDryrun })
280+
} catch (e) {
281+
console.log(chalk.red('X') + ' An error ocurred when run the migration file: ' + e.message)
282+
process.exit(1)
233283
}
234284
})
235285

0 commit comments

Comments
 (0)