Skip to content
Merged

Fix #63

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2.1
parameters:
node-version:
type: string
default: "16.13.2"
default: "24.10.0"
orbs:
node: circleci/node@5.0.0
slack: circleci/slack@4.5.3
Expand Down Expand Up @@ -72,30 +72,30 @@ commands:
jobs:
test:
docker:
- image: circleci/node:16-stretch
- image: cimg/node:24.10.0
steps:
- checkout
- node/install:
node-version: << pipeline.parameters.node-version >>
- run:
name: Audit Dependencies
command: npm audit --production --audit-level=high
- node/install-packages:
cache-path: ./node_modules
override-ci-command: npm install
- run:
name: Running Mocha Tests
name: Audit Dependencies
command: npm audit --production --audit-level=high
- run:
name: Running Tests
command: npm test
build:
docker:
- image: circleci/node:16-stretch
- image: cimg/node:24.10.0
user: root
steps:
- checkout
- node/install:
node-version: << pipeline.parameters.node-version >>
- setup_remote_docker:
version: 19.03.13
version: default
docker_layer_caching: true
# build and push Docker image
- run:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.4.1 (November 14, 2025)
* Fix: support maester attachments in `XML Attachment to JSON` action
* Updated `Sailor` version to 2.7.6
* Updated `@elastic.io/component-commons-library` version to 4.0.0
* Removed `elasticio-node` dependency

## 1.4.0 (June 09, 2023)
* Implemented support `attachments` inside message body for `XML Attachment to JSON` action
* Updated Sailor version to 2.7.1
Expand Down
142 changes: 63 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
# XML Component [![CircleCI](https://circleci.com/gh/elasticio/xml-component.svg?style=svg)](https://circleci.com/gh/elasticio/xml-component)

## Description
iPaaS component to convert between XML and JSON data.
An iPaaS component that converts data between XML and JSON formats.

### Purpose
Allows users to convert XML attachments and strings to and from JSON.
This component has 3 actions allowing users to pass in either generic but well formatted XML/JSON strings or XML attachments
and produces a generic string or attachment of the other file type. The output then can be mapped and used in other components.
This component converts XML attachments or strings to and from JSON. It exposes three actions that accept either well‑formed XML/JSON strings or XML attachments and returns the converted payload as a string or attachment. The result can be consumed by downstream components.

### Requirements and Conversion Behavior
Provided XML document (for `XML to JSON`) should be [well-formed](https://en.wikipedia.org/wiki/Well-formed_document)
in order to be parsed correctly. You will get an error otherwise.
- XML content supplied to the `XML to JSON` action must be [well-formed](https://en.wikipedia.org/wiki/Well-formed_document); invalid XML results in an error.
- JSON inputs must be objects with exactly one field, matching the single root element requirement for XML documents.
- [JSON inputs cannot contain field names that violate XML tag naming rules](https://www.w3schools.com/xml/xml_elements.asp):
- They must start with a letter or underscore.
- They cannot start with the letters `xml` (in any casing).
- They may only contain letters, digits, hyphens, underscores, and periods.

JSON inputs must be objects with exactly one field as XML documents must be contained in a single 'root' tag.
[JSON inputs can not have any field names which are not valid as XML tag names:](https://www.w3schools.com/xml/xml_elements.asp)
* They must start with a letter or underscore
* They cannot start with the letters xml (or XML, or Xml, etc)
* They must only contain letters, digits, hyphens, underscores, and periods

XML attributes on a tag can be read and set by setting an `_attr` sub-object in the JSON.
The inner-text of an XML element can also be controlled with `#` sub-object.
XML attributes on a tag can be represented with an `_attr` object, and element text content can be set with the `_` key.

For example:
```json
Expand All @@ -32,28 +27,22 @@ For example:
}
}
```
is equivalent to

is equivalent to:
```xml
<someTag id="my id">my inner text</someTag>
```

#### Environment variables
* `MAX_FILE_SIZE`: *optional* - Controls the maximum size of an attachment to be read or written in MB.

Defaults to 10 MB where 1 MB = 1024 * 1024 bytes.
* `EIO_REQUIRED_RAM_MB`: *optional* - You can increase memory usage limit for component if you going to work with big files

Defaults to 256 MB where 1 MB = 1024 * 1024 bytes.
- `MAX_FILE_SIZE` (optional): Maximum attachment size, in bytes, that can be read or written. Defaults to 10 MB (`10 * 1024 * 1024` bytes).
- `EIO_REQUIRED_RAM_MB` (optional): Memory usage limit for the component. Defaults to 256 MB.

## Actions

### XML to JSON
Takes XML string and converts it to generic JSON object.

**Limitation:**
Value inside xml tags will be converting into string only, e.g.:
Converts an XML string into a generic JSON object.

given xml
**Limitation:** Values inside XML tags are converted to strings. For example, given:
```xml
<note>
<date>2015-09-01</date>
Expand All @@ -64,7 +53,7 @@ given xml
</note>
```

will be converted into:
the output is:
```json
{
"note": {
Expand All @@ -79,73 +68,68 @@ will be converted into:

### XML Attachment to JSON
#### Configuration Fields

* **Pattern** - (string, optional): RegEx for filtering files by name provided via old attachment mechanism (outside message body)
* **Upload single file** - (checkbox, optional): Use this option if you want to upload a single file
- **Pattern** (string, optional): Regular expression used to filter attachments provided via the legacy attachment mechanism.
- **Upload single file** (checkbox, optional): Enable when the message contains a single attachment object.

#### Input Metadata
If `Upload single file` checked, there will be 2 fields:
* **URL** - (string, required): link to file on Internet or platform

If `Upload single file` unchecked:
* **Attachments** - (array, required): Collection of files to upload, each record contains object with two keys:
* **URL** - (string, required): link to file on Internet or platform

If you going to use this option with static data, you need to switch to Developer mode
<details><summary>Sample</summary>
<p>

```json
{
"attachments": [
{
"url": "https://example.com/files/file1.xml"
},
{
"url": "https://example.com/files/file2.xml"
}
]
}
```
</p>
</details>
When **Upload single file** is enabled:
- **URL** (string, required): Link to the file, either public or internal (including steward/maester storage).

#### Output Metadata
When **Upload single file** is disabled:
- **Attachments** (array, required): Collection of attachment objects.
- **URL** (string, required): Link to the file on the internet or the platform.

Resulting JSON object
If you plan to use this option with static data, switch to Developer mode.
<details><summary>Sample</summary>
<p>

```json
{
"attachments": [
{
"url": "https://example.com/files/file1.xml"
},
{
"url": "https://example.com/files/file2.xml"
}
]
}
```
</p>
</details>

#### Output Metadata
A JSON object built from the parsed XML.

### JSON to XML
Provides an input where a user provides a JSONata expression that should evaluate to an object to convert to JSON.
See [Requirements & Conversion Behavior](#requirements-and-conversion-behavior) for details on conversion logic.
The following options are supported:
* **Upload XML as file to attachments**: When checked, the resulting XML will be placed directly into an attachment.
The attachment information will be provided in both the message's attachments section as well as `attachmentUrl` and `attachmentSize`
will be populated. The attachment size will be described in bytes.
When this box is not checked, the resulting XML will be provided in the `xmlString` field.
* **Exclude XML Header/Description**: When checked, no XML header of the form `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` will be prepended to the XML output.
* **Is the XML file standalone**: When checked, the xml header/description will have a value of `yes` for standalone. Otherwise, the value will be `no`. Has no effect when XML header/description is excluded.

The incoming message should have a single field `input`. When using integrator mode, this appears as the input **JSON to convert** When building mappings in developper mode, one must set the `input` property. E.g.:
Accepts a JSONata expression that must resolve to an object, then converts it to XML. See [Requirements and Conversion Behavior](#requirements-and-conversion-behavior) for more detail.

Options:
- **Upload XML as file to attachments**: When enabled, the resulting XML is stored as an attachment. `attachmentUrl` and `attachmentSize` are provided in the body and the message attachments.
- **Exclude XML Header/Description**: When enabled, the XML declaration (`<?xml version="1.0" encoding="UTF-8" standalone="no"?>`) is omitted.
- **Is the XML file standalone**: Controls the `standalone` attribute in the XML declaration (`yes` or `no`). Ignored when the header is excluded.

Incoming messages must contain a single `input` field. In integrator mode this appears as **JSON to convert**. In developer mode, set the `input` property manually. Example:
```
{
"input": {
"someTag": {
"_attr": {
"id": "my id"
},
"_": "my inner text"
}
}
"someTag": {
"_attr": {
"id": "my id"
},
"_": "my inner text"
}
}
}
```

## Known limitations
- All actions involving attachments are not supported on local agents due to current platform limitations.
- When creating XML files with invalid XML tags, the name of the potentially invalid tag will not be reported.
- When you try to retrieve sample in `XML Attachment to JSON` action and it's size is more then 500Kb, there will be generated new smaller sample with same structure as original
- Actions working with attachments are not supported on local agents due to current platform constraints.
- When creating XML files with invalid XML tags, the invalid tag name is not reported.
- If an attachment used for sampling in `XML Attachment to JSON` exceeds 500 KB, a smaller sample with the same structure is generated.

## Additional Info
Icon made by Freepik from www.flaticon.com
Icon made by Freepik from www.flaticon.com.

## License

Expand Down
2 changes: 1 addition & 1 deletion component.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"title": "XML",
"version": "1.4.0",
"version": "1.4.1",
"description": "Component to convert between XML and JSON data",
"actions": {
"xmlToJson": {
Expand Down
40 changes: 35 additions & 5 deletions lib/actions/attachmentToJson.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { AttachmentProcessor } = require('@elastic.io/component-commons-library')
const { createSchema } = require('genson-js');
const jsf = require('json-schema-faker');
const sizeof = require('object-sizeof');
const { newMessageWithBody } = require('elasticio-node/lib/messages');
const { messages } = require('../utils');
const { readFile, writeFile, stat } = require('fs/promises');
const {
getUserAgent,
Expand Down Expand Up @@ -38,6 +38,33 @@ function checkFileName(self, fileName, pattern) {
const tooLargeErrMsg = (fileName, fileSize) => `Attachment ${fileName} is too large to be processed by XML component. `
+ `File limit is: ${MAX_FILE_SIZE} byte, file given was: ${fileSize} byte.`;

const DEFAULT_ATTACHMENT_ENCODING = 'utf-8';

function resolveEncoding(headers = {}) {
const contentTypeHeader = headers['content-type'] || headers['Content-Type'];
if (!contentTypeHeader) return DEFAULT_ATTACHMENT_ENCODING;
const charsetMatch = /charset=([^;]+)/i.exec(contentTypeHeader);
return charsetMatch ? charsetMatch[1].trim().toLowerCase() : DEFAULT_ATTACHMENT_ENCODING;
}

async function attachmentDataToBuffer(data) {
if (!data) return Buffer.alloc(0);
if (Buffer.isBuffer(data)) return data;
if (typeof data === 'string') return Buffer.from(data);
if (data instanceof ArrayBuffer) return Buffer.from(data);
if (ArrayBuffer.isView(data)) {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
}
if (typeof data[Symbol.asyncIterator] === 'function') {
const chunks = [];
for await (const chunk of data) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
throw new Error('Unsupported attachment response payload type');
}

module.exports.process = async function processAction(msg, cfg) {
const self = this;
const { attachments, body = {} } = msg;
Expand All @@ -61,20 +88,23 @@ module.exports.process = async function processAction(msg, cfg) {
const attachmentProcessor = new AttachmentProcessor(getUserAgent(), msg.id);
for (const file of files) {
self.logger.info('Processing attachment');
let response = await attachmentProcessor.getAttachment(file.url, 'text');
let response = await attachmentProcessor.getAttachment(file.url, 'stream');
this.logger.debug(`For provided filename response status: ${response.status}`);
let responseBodyString = response.data;
let attachmentBuffer = await attachmentDataToBuffer(response.data);
let responseBodyString = attachmentBuffer.toString(resolveEncoding(response.headers));
if (response.status >= 400) {
throw new Error(`Error in making request to ${file.url} Status code: ${response.status}, Body: ${responseBodyString}`);
}
if (!responseBodyString) {
throw new Error(`Empty attachment received for file ${file.fileName || ''}`);
}
const fileSize = response.headers['content-length'];
const fileSizeHeader = response.headers['content-length'];
const fileSize = Number(fileSizeHeader) || attachmentBuffer.length;
response = null;
if (Number(fileSize) > MAX_FILE_SIZE) throw new Error(tooLargeErrMsg(file.fileName || '', fileSize));
let { body: json } = await xml2Json.process(this, responseBodyString);
responseBodyString = null;
attachmentBuffer = null;
await writeFile(tempFile, JSON.stringify(json));
if ((await stat(tempFile)).size > MAX_FILE_SIZE_FOR_SAMPLE && isDebugFlow) {
this.logger.warn('The message size exceeded the sample size limit. To match the limitation we will generate a smaller sample using the structure/schema from the original file.');
Expand All @@ -86,7 +116,7 @@ module.exports.process = async function processAction(msg, cfg) {
json = jsf.generate(schema);
}
this.logger.debug(`Attachment to XML finished, emitting message. ${memUsage()}`);
await self.emit('data', newMessageWithBody(json));
await self.emit('data', messages.newMessageWithBody(json));
}
};

Expand Down
3 changes: 1 addition & 2 deletions lib/actions/jsonToXml.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable no-param-reassign */
const { AttachmentProcessor } = require('@elastic.io/component-commons-library');
const { messages } = require('elasticio-node');
const xml2js = require('xml2js');
const _ = require('lodash');
const { Readable } = require('stream');
const { getUserAgent, MAX_FILE_SIZE } = require('../utils');
const { getUserAgent, MAX_FILE_SIZE, messages } = require('../utils');

module.exports.process = async function process(msg, cfg) {
const { input } = msg.body;
Expand Down
4 changes: 2 additions & 2 deletions lib/actions/jsonToXmlOld.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const eioUtils = require('elasticio-node').messages;
const { messages } = require('../utils');
const xml2js = require('xml2js');
const _ = require('lodash');

Expand Down Expand Up @@ -61,7 +61,7 @@ function processAction(msg, cfg) {

const result = builder.buildObject(jsonToTransform);
this.logger.debug('Successfully converted body to XML');
return eioUtils.newMessageWithBody({
return messages.newMessageWithBody({
xmlString: result,
});
}
Expand Down
5 changes: 5 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ exports.memUsage = () => {
// eslint-disable-next-line max-len
return (`Memory usage - rss: ${f(usage.rss)}, heapTotal: ${f(usage.heapTotal)}, heapUsed: ${f(usage.heapUsed)}, external: ${f(usage.external)}, arrayBuffers: ${f(usage.arrayBuffers)}`);
};

exports.messages = {
newMessageWithBody: (body) => ({ body, headers: {} }),
newEmptyMessage: () => ({ body: {}, headers: {} }),
};
2 changes: 1 addition & 1 deletion lib/xml2Json.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-param-reassign */
const { messages } = require('elasticio-node');
const { messages } = require('../lib/utils');
const xml2js = require('xml2js');

module.exports.process = async function xml2Json(self, xmlString) {
Expand Down
Loading