---
nav: Guide
group:
  title: Plug-in functionality
  order: 3
title: Adapter
order: 10
toc: content
---

## Data format of LogicFlow

In LogicFlow, graph is composed of **nodes** and **edges**.

- For a node, we need to know the **id**, [type](adapter.en.md#type), **position**, **text**, [properties](adapter.en.md#properties) of the node.
- For an edge, we then need to know the **id**, [type](adapter.en.md#type), start node id (*
  *sourceNodeId**), target node id (**targetNodeId**), **text
  **, [properties](adapter.en.md#properties) and the start position of the edge (**startPoint**),
  and the end position of the edge (**endPoint**).

  - Extra data for the fold line `pointsList`, since the fold line can be manually adjusted by the
    user, this field is added to record the exact path of this fold line.

### Type

In LogicFlow, information about the appearance of a node such as its width, height, color, etc. is
not stored in the data, but is represented uniformly by the type of the node. For example, if we
define a node as "startNode" through LogicFlow's customization mechanism, then the current project
should know what the node with type startNode looks like.

### properties

properties is an empty object reserved by LogicFlow for developers to bind any data to. As mentioned
above, the exact appearance of a node is determined by its type. However, when we need to adjust the
appearance of a node based on certain business conditions, we can put those conditions into
properties, and then when customizing the node, we can get the properties through
the `this.props.model` method, and then reset the style of the node based on the contents of the
properties. node based on the contents of the properties.

### Usage

```tsx | pure
lf.render({
  nodes: [
    {
      id: '1',
      type: 'rect',
      x: 100,
      y: 100,
    },
    {
      id: '2',
      type: 'circle',
      x: 300,
      y: 200,
    },
  ],
  edges: [
    {
      id: 'edge1',
      type: 'polyline',
      sourceNodeId: '1',
      targetNodeId: '2',
      startPoint: { x: 150, y: 100 },
      endPoint: { x: 250, y: 200 },
      pointsList: [
        { x: 150, y: 100 },
        { x: 200, y: 100 },
        { x: 200, y: 200 },
        { x: 250, y: 200 },
      ],
    },
  ],
})
```

## What is a Data Conversion Tool

In some cases, the data format generated by LogicFlow may not meet the format required by the
business. For example, if the back-end requires data in the format generated by bpmn-js, you can use
the data conversion tool to convert the data generated by LogicFlow to the data generated by
bpmn-js.

## How to Customize Data Conversion Tools

A custom data conversion tool essentially takes the data passed in by the user and converts it to a
format that LogicFlow recognizes via an `lf.adapterIn` method. Then when generating the data, it
converts LogicFlow's data to the user's incoming data via the `lf.adapterOut` method. So to
customize the data conversion tool we just need to override these two methods again.

```tsx | pure
const lf = new LogicFlow({
  container: document.querySelector("#app"),
});
lf.adapterIn = function(userData) {
  // Here the userData is converted to a format supported by LogicFlow
  return logicFlowData;
};
lf.adapterOut = function(logicFlowData) {
  // Here the data generated by LogicFlow is converted into the format required by the user.
  return userData;
};
// If additional parameters are needed, you can also define them like this
lf.adapterOut = function(logicFlowData, params, ...rest) {
  console.log(params, ...rest);
  return userData;
};
```

## Use the built-in data conversion tools {#use-built-in-data-conversion-tool}

LogicFlow has a built-in generic bpmn-js compatible conversion tool. You can support to display the
graph drawn on LogicFlow on bpmn-js, and also support to display the graph drawn on bpmn-js on
LogicFlow.[LogicFlow2Bpmn](https://github.com/didi/LogicFlow/tree/master/packages/extension/src/bpmn-adapter)

### bpmnAdapter

```tsx | pure
import LogicFlow from "@logicflow/core";
import { BpmnAdapter } from "@logicflow/extension";

LogicFlow.use(BpmnAdapter);

const lf = new LogicFlow();
lf.render();

// Get the converted data via getGraphData.
// Since version 1.2.5, getGraphData has been added as an input parameter to ensure that certain adapterOut's are executed correctly, e.g. the adapterOut of the bpmn-adapter in this case has an optional input parameter "retainedFields".
// This means that a field that appears in this array will not be considered a node but a property when its value is an array or an object. We have defined some default property fields such as "properties", "startPoint", "endPoint", "pointsList", which are obviously not sufficient for data processing.
// So in order to ensure that certain node attributes are processed properly in the exported data, pass in an array of attribute reserved fields as needed. e.g. lf.getGraphData(['attribute-a', 'attribute-b'])

lf.getGraphData();
```

### Example of conversion results

<!-- TODO -->
<a href="https://examples.logic-flow.cn/demo/dist/examples/#/extension/adapter?from=doc" target="_blank"> Go to CodeSandbox for examples </a>

## New BPMNAdapter

### what's the difference?

The new bpmn-adapter has been upgraded somewhat from the previous `BpmnAdapter` by allowing an
additional object to be passed in as an input parameter when calling adapterIn and adapterOut

```tsx | pure
type ExtraPropsType = {
  /**
   * retainedAttrsFields retainedAttrsFields will be merged with the default defaultRetainedProperties:.
   * ["properties", "startPoint", "endPoint", "pointsList"].
   * This means that a field that appears in this array will not be considered a node but a property when its value is an array or an object.
   */
  retainedAttrsFields?: string[];

  /**
   * excludeFields will be merged with the default defaultExcludeFields.
   * Fields appearing in this array will be ignored in exports
   */
  excludeFields?: {
    in?: Set<string>;
    out?: Set<string>;
  };

  /**
   * transformer is an array, each item in the array is an object, the object contains in and out two properties
   * in function receives two parameters key and data, key is the key of the current processing object, that is, the node name, data for the current object, when the import will call this function, the imported data processing, to get the desired data
   * out function receives one parameter data, data for the current processing node data, when the export will call this function, the need to export the data processing, to get our desired data
   */
  transformer?: {
    [key: string]: {
      in?: (key: string, data: any) => any;
      out?: (data: any) => any;
    }
  };

  mapping?: {
    in?: {
      [key: string]: string;
    };
    out?: {
      [key: string]: string;
    };
  };
};
```

### Usage

```tsx | pure
// step 1 Introducing the adapter plugin
import { BPMNAdapter } from '@logicflow/extension';
// step 2 Registration Plugin
LogicFlow.use(BPMNAdapter)

//...
// step 3-1 Calling the default export
const xmlResult = lf.adapterOut(lf.getGraphRawData())
```

很多时候，我们可能需要一些额外的参数帮助我们正确的进行导入导出，这时我们需要用到上面提到的adapterOut额外的入参ExtraPropsType

```tsx | pure
// step 3-2 Use the second parameter ExtraPropsType of adapterOut to export the

// Suppose we have a panels property in our node attribute, which is an array, but we don't want it to be treated as a node, but as an attribute, then we can do this
const extraProps = {
  retainedAttrsFields: ['panels']
}
// Suppose we have a runboost attribute in our node properties, but we want to export without it, then we can do something like this
extraProps.excludeFields = {
  out: new Set(['runboost'])
}
```

If we want to export a sequenceFlow containing judgment conditions in the correct XML format, and
import it with the correct processing to get the data we want, then we can do this:

``` xml
<!-- Sequential Streaming XML Data Format： -->

<bpmn:sequenceFlow id="SequenceFlow_0j5q1qk" sourceRef="StartEvent_1" targetRef="Activity_0j5q1qk"/>

<!-- When judgment conditions are included -->

<bpmn:sequenceFlow id="SequenceFlow_0j5q1qk" sourceRef="StartEvent_1" targetRef="Activity_0j5q1qk">
    <bpmn:conditionExpression xsi:type="bpmn2:tFormalExpression">
        <![CDATA[${runboost === '1'}]]>
    </bpmn:conditionExpression>
</bpmn:sequenceFlow>
<!-- or -->
<bpmn:sequenceFlow id="SequenceFlow_0j5q1qk" sourceRef="StartEvent_1" targetRef="Activity_0j5q1qk">
    <bpmn:conditionExpression xsi:type="bpmn2:tFormalExpression">
        runboost === '1'
    </bpmn:conditionExpression>
</bpmn:sequenceFlow>
```

```tsx | pure
/**
 * Taking the example of a sequence flow containing a judgment condition.
 * When doing the import, we need to extract the properties within the <bpmn:sequenceFlow>'s child element, <bpmn:conditionExpression>, * and
 * and eventually into the properties attribute of the parent element bpmn:sequenceFlow
 * so the content within <bpmn:conditionExpression> that we actually need to process when importing.
 * it is processed and the data is merged into the properties property of bpmn:sequenceFlow
 */
extraProps.transformer = {
  'bpmn:sequenceFlow': {
    out(data: any) {
      const { properties: { expressionType, condition } } = data;
      if (condition) {
        if (expressionType === 'cdata') {
          return {
            json:
              `<bpmn:conditionExpression xsi:type="bpmn2:tFormalExpression"><![CDATA[\${${
                condition
              }}]]></bpmn:conditionExpression>`,
          };
        }
        return {
          json: `<bpmn:conditionExpression xsi:type="bpmn2:tFormalExpression">${condition}</bpmn:conditionExpression>`,
        };
      }
      return {
        json: '',
      };
    },
  },
  // The returned data is merged into the properties attribute of the parent element bpmn:sequenceFlow
  'bpmn:conditionExpression': {
    in(_key: string, data: any) {
      let condition = '';
      let expressionType = '';
      if (data['#cdata-section']) {
        expressionType = 'cdata';
        condition = /^\$\{(.*)\}$/g.exec(data['#cdata-section'])?.[1] || '';
      } else if (data['#text']) {
        expressionType = 'normal';
        condition = data['#text'];
      }
      return {
        '-condition': condition,
        '-expressionType': expressionType,
      };
    },
  },
}
```

*** The principle of data import and export is that parent elements are handled when exporting;
child elements are handled when importing. ***

- When exporting, we need to get the data we need from the parent element's attributes and splice
  out the child element.
- When importing, we need to extract the data we need from the child element and put it into the
  attribute of the parent element.

After configuring the required extraProps, we need to register the plugin by passing in the

```tsx | pure
LogicFlow.use(BPMNAdapter, extraProps)
```

Currently, we have built-in transformers **(for reference only)**:

> Note: The built-in transformers are for reference only, these transformers are used to work with
> the bpmn node plugin in the process of writing, inside such
> as `timerType`, `timerValue`, `definitionId`, are configured through the bpmn node plugin's
> definitionConfig. When actually using the data transformation plugin, you can not use the bpmn node
> plugin, through your own way to add properties to the node, customize the transformer to achieve the
> data transformation that meets your needs.

```tsx | pure

let defaultTransformer: TransformerType = {
  'bpmn:startEvent': {
    out(data: any) {
      const { properties } = data;
      return defaultTransformer[properties.definitionType]?.out(data) || {};
    },
  },
  'bpmn:intermediateCatchEvent': {
    out(data: any) {
      const { properties } = data;
      return defaultTransformer[properties.definitionType]?.out(data) || {};
    },
  },
  'bpmn:intermediateThrowEvent': {
    out(data: any) {
      const { properties } = data;
      return defaultTransformer[properties.definitionType]?.out(data) || {};
    },
  },
  'bpmn:boundaryEvent': {
    out(data: any) {
      const { properties } = data;
      return defaultTransformer[properties.definitionType]?.out(data) || {};
    },
  },
  'bpmn:sequenceFlow': {
    out(data: any) {
      const {
        properties: { expressionType, condition },
      } = data;
      if (condition) {
        if (expressionType === 'cdata') {
          return {
            json:
              `<bpmn:conditionExpression xsi:type="bpmn2:tFormalExpression"><![CDATA[\${${
                condition
              }}]]></bpmn:conditionExpression>`,
          };
        }
        return {
          json: `<bpmn:conditionExpression xsi:type="bpmn2:tFormalExpression">${condition}</bpmn:conditionExpression>`,
        };
      }
      return {
        json: '',
      };

    },
  },
  'bpmn:timerEventDefinition': {
    out(data: any) {
      // Here timerType, timerValue, definitionId 
      // These are all properties configured in the definitionConfig when extending the node via the Bpmn node plugin, so you need to modify the out function according to your own settings when you use it.
      const {
        properties: { timerType, timerValue, definitionId },
      } = data;

      const typeFunc = () => `<bpmn:${timerType} xsi:type="bpmn:tFormalExpression">${timerValue}</bpmn:${timerType}>`;

      return {
        json:
          `<bpmn:timerEventDefinition id="${definitionId}"${
            timerType && timerValue
              ? `>${typeFunc()}</bpmn:timerEventDefinition>`
              : '/>'}`,
      };
    },
    in(key: string, data: any) {
      const definitionType = key;
      const definitionId = data['-id'];
      let timerType = '';
      let timerValue = '';
      for (const key of Object.keys(data)) {
        if (key.includes('bpmn:')) {
          [, timerType] = key.split(':');
          timerValue = data[key]?.['#text'];
        }
      }
      return {
        '-definitionId': definitionId,
        '-definitionType': definitionType,
        '-timerType': timerType,
        '-timerValue': timerValue,
      };
    },
  },
  'bpmn:conditionExpression': {
    in(_key: string, data: any) {
      let condition = '';
      let expressionType = '';
      if (data['#cdata-section']) {
        expressionType = 'cdata';
        condition = /^\$\{(.*)\}$/g.exec(data['#cdata-section'])?.[1] || '';
      } else if (data['#text']) {
        expressionType = 'normal';
        condition = data['#text'];
      }

      return {
        '-condition': condition,
        '-expressionType': expressionType,
      };
    },
  },
};

```

The incoming transformer and the default transformer are passed through the

```tsx | pure

const mergeInNOutObject = (target: any, source: any): TransformerType => {
  const sourceKeys = Object.keys(source);
  sourceKeys.forEach((key) => {
    if (target[key]) {
      const { in: fnIn, out: fnOut } = source[key];
      if (fnIn) {
        target[key].in = fnIn;
      }
      if (fnOut) {
        target[key].out = fnOut;
      }
    } else {
      target[key] = source[key];
    }
  });
  return target;
};
```

carry out a merger
