Environments and migrations > Write and test migration scripts

    Write and test migration scripts

    Creating your first migration script

    For this example, we're starting with a blank DatoCMS project, and then progressively add models/records using migrations.

    There are two ways to create a migration script:

    1. By writing it manually, or

    2. By having the CLI automatically generate it for you.

    We'll cover both methods in detail below.

    Leverage TypeScript to simplify your work!

    Since our Content Management API client is fully typed, we strongly suggest you to have your migration scripts written in TypeScript. You will get auto-completion suggestions on every call to an endpoint, and type checks for free.

    If you're project has a tsconfig.json file, the datocms migrations:new command will automatically create migration scripts in TypeScript, but you can also manually pass the --ts flag to the migrations:new command.

    Option 1: Write a migration script manually

    Let's create an Article model with a simple Title field. With the CLI tool successfully set up, run the following command inside your project:

    $ npx datocms migrations:new 'create article model' --api-token=<YOUR-API-TOKEN>
    Created migrations/1591173668_createArticleModel.js

    This will create inside your migrations directory a new script named <TIMESTAMP>_createArticleModel.js.

    Let's take a look at its content:

    'use strict';
    /** @param client { import("@datocms/cli/lib/cma-client-node").Client } */
    module.exports = async (client) => {
    // DatoCMS migration script
    // For more examples, head to our Content Management API docs:
    // https://www.datocms.com/docs/content-management-api
    // Create an Article model:
    // https://www.datocms.com/docs/content-management-api/resources/item-type/create
    const articleModel = await client.itemTypes.create({
    name: 'Article',
    api_key: 'article',
    });
    // Create a Title field (required):
    // https://www.datocms.com/docs/content-management-api/resources/field/create
    const titleField = await client.fields.create(articleModel, {
    label: 'Title',
    api_key: 'title',
    field_type: 'string',
    validators: {
    required: {},
    },
    });
    // Create an Article record:
    // https://www.datocms.com/docs/content-management-api/resources/item/create
    const article = await client.items.create({
    item_type: articleModel,
    title: 'My first article!',
    });
    }

    The script exports an async function with a client argument, which is an instance of our Content Management API client.

    Incidentally, the body of the function is already filled with everything we need for this particular example, but of course, you can rewrite the migration script to your liking using any method available in our Content Management API to produce the desired results.

    Custom migration script template?

    If you would like to scaffold new migration scripts from a custom template instead of the default one, feel free to pass the --template flag. Or, even better, you can add it as a default setting to you profile with the datocms profile:set command, so that the choice will propagate to every other team member.

    Running the migration script

    To execute the migration, run the following command:

    $ npx datocms migrations:run --destination=feature-branch --api-token=<YOUR-API-TOKEN>

    Here's the result:

    Upon execution, the command does the following:

    • Forks the primary environment into a new sandbox environment called feature-branch;

    • Runs any pending migrations inside the sandbox environment.

    How the CLI keeps track of already-run migrations?

    To track which migrations have already run in a specific environment, the CLI creates a special schema_migration model in your project. After each migration script completes, it creates a record referencing the name of the script itself.

    You can configure the name of the model with the --migrations-model flag, or configure your profile accordingly with the datocms profile:set command!

    To verify that only pending migrations are executed, we can re-run the same command and see the result:

    $ npx datocms migrations:run --destination=feature-branch
    Migrations will be run in "feature-branch" sandbox environment
    Creating a fork of "main" environment called "feature-branch"... !
    › Error: Environment "feature-branch" already exists!
    › Try this:
    › * To execute the migrations inside the existing environment, run "datocms migrations:run --source=feature-branch --in-place"
    › * To delete the environment, run "datocms environments:destroy feature-branch"

    Ouch! The sandbox environment feature-branch already exists, so the command failed. We can follow the CLI suggestion to re-run the migrations inside the already existing sandbox environment:

    $ npx datocms migrations:run --source=feature-branch --in-place
    Migrations will be run in "feature-branch" sandbox environment
    No new migration scripts to run, skipping operation

    As you can see, no migration gets executed, as our script has already been run in this environment!

    Programmatically parse the CLI output

    Remember that you can always add the --json flag to any CLI command, to get a JSON output, easily parsable by tools like jq.

    Option 2: Autogenerate a migration script

    Let's create a new migration script to add a new Author model, and an Author field on the article. This time, we're going to use the --autogenerate flag on the migrations:new command.

    The --autogenerate flag takes two environment names as an argument:

    $ datocms migrations:new --help
    [...]
    --autogenerate=<value>
    Auto-generates script by diffing the schema of two environments
    Examples:
    * --autogenerate=foo finds changes made to sandbox environment 'foo' and
    applies them to primary environment
    * --autogenerate=foo:bar finds changes made to environment 'foo' and applies
    them to environment 'bar'

    So first we need to make a copy of our feature-branch environment (let's call it with-authors):

    $ datocms environments:fork feature-branch with-authors
    Creating a fork of "feature-branch" called "with-authors"... done

    Then add our Author and Author field on the with-authors environment using the regular DatoCMS interface, and then run the following command to generate a migration script:

    $ npx datocms migrations:new addAuthors --autogenerate=with-authors:feature-branch --api-token=<YOUR-API-TOKEN>
    Writing "migrations/1653062813_addAuthors.js"... done

    Let's see the result:

    /** @param client { import("@datocms/cli/lib/cma-client-node").Client } */
    module.exports = async function (client) {
    const newFields = {};
    const newItemTypes = {};
    const newMenuItems = {};
    console.log('Create new models/block models');
    console.log('Create model "Author" (`author`)');
    newItemTypes['531556'] = await client.itemTypes.create(
    {
    name: 'Author',
    api_key: 'author',
    all_locales_required: true,
    collection_appearance: 'table',
    },
    { skip_menu_item_creation: 'true' },
    );
    console.log('Creating new fields/fieldsets');
    console.log('Create Single-line string field "Name" (`name`) in model "Author" (`author`)');
    newFields['2791804'] = await client.fields.create(newItemTypes['531556'], {
    label: 'Name',
    field_type: 'string',
    api_key: 'name',
    validators: { required: {} },
    appearance: {
    addons: [],
    editor: 'single_line',
    parameters: { heading: true },
    type: 'title',
    },
    default_value: '',
    });
    console.log('Create Single link field "Author" (`author`) in model "Blog Post" (`blog_post`)');
    newFields['2791806'] = await client.fields.create('810907', {
    label: 'Author',
    field_type: 'link',
    api_key: 'author',
    validators: {
    item_item_type: {
    on_publish_with_unpublished_references_strategy: 'fail',
    on_reference_unpublish_strategy: 'delete_references',
    on_reference_delete_strategy: 'delete_references',
    item_types: [newItemTypes['531556'].id],
    },
    required: {},
    },
    appearance: { addons: [], editor: 'link_select', parameters: {} },
    });
    console.log('Finalize models/block models');
    console.log('Update model "Author" (`author`)');
    await client.itemTypes.update(newItemTypes['531556'], {
    title_field: newFields['2791804'],
    });
    console.log('Manage menu items');
    console.log('Create menu item "Authors"');
    newMenuItems['265140'] = await client.menuItems.create({
    label: 'Authors',
    item_type: newItemTypes['531556'],
    });
    };

    Wow! Thanks CLI, that's a lot of code for free! 😀

    If we run the migrations again in our feature-branch environment, we can verify that the script is indeed working:

    $ npx datocms migrations:run --source=feature-branch --in-place --api-token=<YOUR-API-TOKEN>
    Migrations will be run in "feature-add-article-model" sandbox environment
    Running migration "1653062813_addAuthors.js"...
    Create new models/block models
    Create model "Author" (`author`)
    Creating new fields/fieldsets
    Create Single-line string field "Name" (`name`) in model "Author" (`author`)
    Finalize models/block models
    Update model "Author" (`author`)
    Manage menu items
    Create menu item "Authors"
    done
    Automigrations are only for schema changes!

    The --autogenerate flag will not take into account changes made to records and uploads! If you need that, you you are required to write your own migration script manually — or extend the one that the autogeneration tool created for you.

    Re-applying a migration script

    Suppose that you need to make a change to a migration script after running it.

    Since we're working on a sandbox, to test the new script, we can simply delete the current sandbox, fork a new one from the primary environment and re-run the migrations again:

    $ npx datocms environments:destroy feature-branch
    Destroying environment "feature-branch"... done
    $ npx datocms migrations:run --destination=feature-branch
    Migrations will be run in "feature-branch" sandbox environment
    Creating a fork of "main" environment called "feature-branch"... done
    Creating "schema_migration" model... done
    Running migration "1653061497_createArticleModel.js"... done
    Running migration "1653062813_addAuthors.js"... done
    Successfully run 2 migration scripts
    Done!

    Adapt your website/app to the schema changes

    It goes without saying that you also need to work on your website/app to adapt it to the changes you just made. We suggest to work on a Git feature branch, and store the the migrations directory in the same repo as your frontend code.

    All of our APIs and integrations offer a way to point to a sandbox environment instead of the primary one.

    For example, if you're using our GraphQL Content Delivery API, you can explicitly read data from a specific environment using one of the following endpoints:

    https://graphql.datocms.com/environments/{ENVIRONMENT-NAME}
    https://graphql.datocms.com/environments/{ENVIRONMENT-NAME}/preview

    Once everything is working as expected, we can ship everything production.

    Deleting a sandbox environment

    After you've run your tests you might need to programmatically delete a sandbox environment.

    In this case you can simply run:

    $ npx datocms environments:destroy <SANDBOX-ENVIRONMENT-NAME>

    Learn more about migrations

    Check out this tutorial on how to migrate your content schema using scripts: