.. include:: alias.rst

Migrations of Apps and Groups
=============================

As of system version ``5.7.0``, app developers can include JavaScript migration
scripts within their bundles to handle major updates without disrupting users'
parameterized Tasks. These scripts enable version-specific or general migrations
for Apps and Groups, allowing for seamless updates and enhancements.

Without migration scripts, the system updates existing Task as follows:

- Newly added parameters are assigned default values specified in the *.lf* file to all existing Tasks. If the parameter defaults to *nil*, a previously fully configured App is no longer fully parameterized. This is usually a breaking change for the user, since re-parameterization of all occurrences of the App is required, before the affected Tasks can be executed.
- Newly removed parameters are deleted from all existing Tasks of a user.

Migration scripts allow to handle further scenarios, such as:

- Adding new parameters with dependencies to other parameters of the same App or new parameters to an existing struct.
- Renaming or reducing the value range of parameters.
- Modifying parameter fields of child Apps of a Group G, in case they previously were access paths referencing G.

By implementing migration scripts, you can confidently update and enhance your
Apps while providing a seamless experience for Desk users.

Migration scripts will be executed whenever a new version of a bundle is
installed or when a Task is imported and its bundle versions differ from the
currently installed bundle.

The migration scripts are placed in the `migrations` folder of the bundle:

.. code-block:: shell

  .
  └── bundles
      └── test
          ├── manifest.json
          ├── migrations
          │   ├── index.mjs
          │   └── utils.mjs
          ├── resources
          └── sources
              └── Test.lf

During migration, you can use only the resources located in the `migrations`
folder and the `manifest.json`. The process runs in a sandboxed environment
without external connections. However, you can leverage shared migration
functions, see :ref:`import-other-migration-scripts` for further information.

Migration scripts
+++++++++++++++++

The `migrations` folder must contain an `index.mjs` file with a default export.
The default export must be an object with state machine names as keys
and corresponding migration functions as values. The migration function must
be an async function receiving the element to be migrated and a list of
parent elements, where the first element in the list is the direct parent:

.. code-block:: typescript

   (element: App | Group, parents: (App | Group)[]) => Promise<void>

.. note::

   A migration function must not run longer than 1 second, otherwise it will
   timeout and migration for this element will be aborted.

When migrating a Task, the system will traverse all elements, i.e. all
contained Groups and Apps, and for each element call a corresponding migration
function, if it exists. The elements are traversed depth-first, therefore, when
a migration function is executed, it is ensured that all parents have already
been migrated.

If the element to be migrated is an App it will contain the following fields:

.. code-block:: typescript

   interface App {
     id: string
     name: string
     link: string
     bundle: {
       name: string
       version: string
     }
     parameter: any
   }

If the element to be migrated is a Group it will contain the following fields:

.. code-block:: typescript

   interface Group {
     id: string
     name: string
     libraryItemId: string
     bundle: {
       name: string
       version: string
     }
     parameter: any
     containers: [{ elements: (App | Group | TimelineLink)[] }]
   }

The children of Groups can contain timeline links, which are links to other
Tasks, however they will not be migrated. They are just included for
completeness. A timeline link will contain the following fields:

.. code-block:: typescript

   interface TimelineLink {
     id: string
     name: string
     timelineLink: string
   }

An element can be migrated by simply modifying the fields of the given element.
The following fields are modifiable:

* name
* link for Apps
* libraryItemId for Groups
* parameter
* parameter of children for Groups, but only fields that contain an access path
  to the Group to be migrated

Modifying any other field will have no effect.

Usually, the bundle version will contain the version of the currently installed
bundle. For Tasks that are migrated from a system with version < ``5.7.0`` the
bundle name will be "unknown" and the version will be "0.0".

.. note::

   Whenever the bundle version changes all registered migration functions will
   potentially be executed, even though the corresponding App or Group might
   not have changed. Therefore it is crucial that migration scripts take the
   bundle version into account.

One way to ensure this is as follows:

.. code-block:: javascript

   async function migrate_my_app(element) {
     if (element.bundle.version === '1.0') {
       // perform migration from version 1.0 to current version
     }
   }

.. warning::

   Migration scripts must not read from stdin.

   The system uses stdin and stdout to communicate with the migration process.
   Reading from stdin might interfere and cause migration to fail.

.. warning::

   When a migration script fails the action that triggered the task migration
   will fail, too. A migration script will fail, for example, because it throws
   an error, the migration function is not async, or migration function takes
   too long to execute. Actions that will trigger task migration are bundle
   installation and task import.

.. _import-other-migration-scripts:
Importing other migration scripts
+++++++++++++++++++++++++++++++++

Migration scripts can reuse functionality from other bundles by importing
the corresponding functions or variables.

For instance, a `utils.mjs` file placed in bundle A can be utilized in a
migration script in bundle B. To ensure that `utils.mjs` from bundle A is
available during the execution of bundle B's migration script, you need to
declare a dependency on bundle A in the manifest of bundle B.

.. code-block:: json

  {
    "name": "B",
    "version": "1.0",
    "dependencies": ["A"],
    "globalComponents": []
  }

You can then import functions from `utils.mjs`:

.. code-block:: javascript

   import { some_function_in_A } from 'A/migrations/utils.mjs'

Example
+++++++

Consider the following App:

.. code-block:: lf

   my_app {
     ...

     parameterType {
       string param_1;
       string param_2;
       float param_3; -- value in [0, 1]
     }

     ...
   }

It is contained in some bundle with version "1.0". Assume we want to 
change the parameter type in version "1.1" to the following:

.. code-block:: lf

   my_app {
     ...

     parameterType {
       string new_param_with_default;
       string new_param_without_default;
       string new_param_2;
       float param_3; -- value in [0, 100]
     }

     ...
   }

We want to add two fields `new_param_with_default` and
`new_param_without_default`, delete `param_1`, rename `param_2` to
`new_param_2` and modify the value range of `param_3`. This could be
accomplished by adding the following migration script:

.. code-block:: javascript

   const migrations = {
     my_app: migrate_my_app,
   }

   async function migrate_my_app(element) {
     // check that current version of the given element is 1.0
     if (element.bundle.version === '1.0') {
       // logic for migrating element to version 1.1:

       // add a new parameter field with default value
       element.parameter.new_param_with_default = "foo"

       // add a new parameter field without default value
       element.parameter.new_param_without_default = null

       // delete a parameter field
       delete element.parameter.param_1

       // rename a parameter field
       element.parameter.new_param_2 = element.parameter.param_2
       delete element.parameter.param_2

       // modify the value of a parameter field
       element.parameter.param_3 = element.parameter.param_3 * 100
     }
   }

   export default migrations
