diff --git a/package-lock.json b/package-lock.json
index 7ecbaafb92..d42744d08b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13837,6 +13837,15 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "optional": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
"array-union": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
@@ -13872,6 +13881,21 @@
}
}
},
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "optional": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "optional": true
+ },
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -13934,6 +13958,58 @@
"worker-rpc": "^0.1.0"
}
},
+ "fork-ts-checker-webpack-plugin-v5": {
+ "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
+ "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
+ "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
+ "optional": true,
+ "requires": {
+ "@babel/code-frame": "^7.8.3",
+ "@types/json-schema": "^7.0.5",
+ "chalk": "^4.1.0",
+ "cosmiconfig": "^6.0.0",
+ "deepmerge": "^4.2.2",
+ "fs-extra": "^9.0.0",
+ "memfs": "^3.1.2",
+ "minimatch": "^3.0.4",
+ "schema-utils": "2.7.0",
+ "semver": "^7.3.2",
+ "tapable": "^1.0.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "optional": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "optional": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "optional": true,
+ "requires": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
@@ -13968,6 +14044,12 @@
"slash": "^2.0.0"
}
},
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "optional": true
+ },
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -13996,6 +14078,16 @@
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
},
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "optional": true,
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
"micromatch": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@@ -14031,6 +14123,17 @@
}
}
},
+ "schema-utils": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
+ "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
+ "optional": true,
+ "requires": {
+ "@types/json-schema": "^7.0.4",
+ "ajv": "^6.12.2",
+ "ajv-keywords": "^3.4.1"
+ }
+ },
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -14041,6 +14144,15 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="
},
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "optional": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
"to-regex-range": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
@@ -14134,6 +14246,12 @@
"requires": {
"tslib": "^1.8.1"
}
+ },
+ "universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "optional": true
}
}
},
@@ -23641,124 +23759,6 @@
}
}
},
- "fork-ts-checker-webpack-plugin-v5": {
- "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
- "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
- "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
- "optional": true,
- "requires": {
- "@babel/code-frame": "^7.8.3",
- "@types/json-schema": "^7.0.5",
- "chalk": "^4.1.0",
- "cosmiconfig": "^6.0.0",
- "deepmerge": "^4.2.2",
- "fs-extra": "^9.0.0",
- "memfs": "^3.1.2",
- "minimatch": "^3.0.4",
- "schema-utils": "2.7.0",
- "semver": "^7.3.2",
- "tapable": "^1.0.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "optional": true,
- "requires": {
- "color-convert": "^2.0.1"
- }
- },
- "chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "optional": true,
- "requires": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "optional": true,
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "optional": true
- },
- "fs-extra": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
- "optional": true,
- "requires": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- }
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "optional": true
- },
- "jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "optional": true,
- "requires": {
- "graceful-fs": "^4.1.6",
- "universalify": "^2.0.0"
- }
- },
- "schema-utils": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
- "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
- "optional": true,
- "requires": {
- "@types/json-schema": "^7.0.4",
- "ajv": "^6.12.2",
- "ajv-keywords": "^3.4.1"
- }
- },
- "semver": {
- "version": "7.3.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
- "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
- "optional": true,
- "requires": {
- "lru-cache": "^6.0.0"
- }
- },
- "supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "optional": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- },
- "universalify": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
- "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
- "optional": true
- }
- }
- },
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -29714,6 +29714,23 @@
"@types/yargs-parser": "*"
}
},
+ "acorn": {
+ "version": "5.7.4",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
+ "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg=="
+ },
+ "escodegen": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
+ "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
+ "requires": {
+ "esprima": "^4.0.1",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.6.1"
+ }
+ },
"jest-util": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz",
@@ -29733,6 +29750,44 @@
"source-map": "^0.6.0"
}
},
+ "jsdom": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
+ "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
+ "requires": {
+ "abab": "^2.0.0",
+ "acorn": "^5.5.3",
+ "acorn-globals": "^4.1.0",
+ "array-equal": "^1.0.0",
+ "cssom": ">= 0.3.2 < 0.4.0",
+ "cssstyle": "^1.0.0",
+ "data-urls": "^1.0.0",
+ "domexception": "^1.0.1",
+ "escodegen": "^1.9.1",
+ "html-encoding-sniffer": "^1.0.2",
+ "left-pad": "^1.3.0",
+ "nwsapi": "^2.0.7",
+ "parse5": "4.0.0",
+ "pn": "^1.1.0",
+ "request": "^2.87.0",
+ "request-promise-native": "^1.0.5",
+ "sax": "^1.2.4",
+ "symbol-tree": "^3.2.2",
+ "tough-cookie": "^2.3.4",
+ "w3c-hr-time": "^1.0.1",
+ "webidl-conversions": "^4.0.2",
+ "whatwg-encoding": "^1.0.3",
+ "whatwg-mimetype": "^2.1.0",
+ "whatwg-url": "^6.4.1",
+ "ws": "^5.2.0",
+ "xml-name-validator": "^3.0.0"
+ }
+ },
+ "parse5": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
+ },
"slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
@@ -29742,6 +29797,37 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "tr46": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
+ "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "webidl-conversions": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
+ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
+ },
+ "whatwg-url": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
+ "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
+ "requires": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^1.0.1",
+ "webidl-conversions": "^4.0.2"
+ }
+ },
+ "ws": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz",
+ "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==",
+ "requires": {
+ "async-limiter": "~1.0.0"
+ }
}
}
},
@@ -31837,100 +31923,6 @@
}
}
},
- "jsdom": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
- "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
- "requires": {
- "abab": "^2.0.0",
- "acorn": "^5.5.3",
- "acorn-globals": "^4.1.0",
- "array-equal": "^1.0.0",
- "cssom": ">= 0.3.2 < 0.4.0",
- "cssstyle": "^1.0.0",
- "data-urls": "^1.0.0",
- "domexception": "^1.0.1",
- "escodegen": "^1.9.1",
- "html-encoding-sniffer": "^1.0.2",
- "left-pad": "^1.3.0",
- "nwsapi": "^2.0.7",
- "parse5": "4.0.0",
- "pn": "^1.1.0",
- "request": "^2.87.0",
- "request-promise-native": "^1.0.5",
- "sax": "^1.2.4",
- "symbol-tree": "^3.2.2",
- "tough-cookie": "^2.3.4",
- "w3c-hr-time": "^1.0.1",
- "webidl-conversions": "^4.0.2",
- "whatwg-encoding": "^1.0.3",
- "whatwg-mimetype": "^2.1.0",
- "whatwg-url": "^6.4.1",
- "ws": "^5.2.0",
- "xml-name-validator": "^3.0.0"
- },
- "dependencies": {
- "acorn": {
- "version": "5.7.4",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
- "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg=="
- },
- "escodegen": {
- "version": "1.14.3",
- "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
- "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
- "requires": {
- "esprima": "^4.0.1",
- "estraverse": "^4.2.0",
- "esutils": "^2.0.2",
- "optionator": "^0.8.1",
- "source-map": "~0.6.1"
- }
- },
- "parse5": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
- "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "optional": true
- },
- "tr46": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
- "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
- "requires": {
- "punycode": "^2.1.0"
- }
- },
- "webidl-conversions": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
- "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
- },
- "whatwg-url": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
- "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
- "requires": {
- "lodash.sortby": "^4.7.0",
- "tr46": "^1.0.1",
- "webidl-conversions": "^4.0.2"
- }
- },
- "ws": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz",
- "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==",
- "requires": {
- "async-limiter": "~1.0.0"
- }
- }
- }
- },
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
diff --git a/packages/nodes-base/credentials/KoBoToolboxApi.credentials.ts b/packages/nodes-base/credentials/KoBoToolboxApi.credentials.ts
new file mode 100644
index 0000000000..9fdb6bf8c6
--- /dev/null
+++ b/packages/nodes-base/credentials/KoBoToolboxApi.credentials.ts
@@ -0,0 +1,26 @@
+import {
+ ICredentialType,
+ NodePropertyTypes,
+} from 'n8n-workflow';
+
+export class KoBoToolboxApi implements ICredentialType {
+ name = 'koBoToolboxApi';
+ displayName = 'KoBoToolbox API Token';
+ // See https://support.kobotoolbox.org/api.html
+ documentationUrl = 'koBoToolbox';
+ properties = [
+ {
+ displayName: 'API root URL',
+ name: 'URL',
+ type: 'string' as NodePropertyTypes,
+ default: 'https://kf.kobotoolbox.org/',
+ },
+ {
+ displayName: 'API Token',
+ name: 'token',
+ type: 'string' as NodePropertyTypes,
+ default: '',
+ hint: 'You can get your API token at https://[api-root]/token/?format=json (for a logged in user)',
+ },
+ ];
+}
diff --git a/packages/nodes-base/nodes/KoBoToolbox/FormDescription.ts b/packages/nodes-base/nodes/KoBoToolbox/FormDescription.ts
new file mode 100644
index 0000000000..daf0536204
--- /dev/null
+++ b/packages/nodes-base/nodes/KoBoToolbox/FormDescription.ts
@@ -0,0 +1,202 @@
+import {
+ INodeProperties,
+} from 'n8n-workflow';
+
+export const formOperations: INodeProperties[] = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: [
+ 'form',
+ ],
+ },
+ },
+ options: [
+ {
+ name: 'Get',
+ value: 'get',
+ description: 'Get a form',
+ },
+ {
+ name: 'Get All',
+ value: 'getAll',
+ description: 'Get all forms',
+ },
+ ],
+ default: 'get',
+ },
+];
+
+export const formFields: INodeProperties[] = [
+
+ /* -------------------------------------------------------------------------- */
+ /* form:get */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Form ID',
+ name: 'formId',
+ type: 'string',
+ required: true,
+ default: '',
+ displayOptions: {
+ show: {
+ resource: [
+ 'form',
+ ],
+ operation: [
+ 'get',
+ ],
+ },
+ },
+ description: 'Form ID (e.g. aSAvYreNzVEkrWg5Gdcvg)',
+ },
+ /* -------------------------------------------------------------------------- */
+ /* form:getAll */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ required: true,
+ default: false,
+ displayOptions: {
+ show: {
+ resource: [
+ 'form',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ description: 'Whether to return all results',
+ },
+ {
+ displayName: 'Limit',
+ name: 'limit',
+ type: 'number',
+ required: false,
+ typeOptions: {
+ maxValue: 3000,
+ },
+ displayOptions: {
+ show: {
+ resource: [
+ 'form',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ returnAll: [
+ false,
+ ],
+ },
+ },
+ default: 1000,
+ description: 'The number of results to return',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ placeholder: 'Add Option',
+ type: 'collection',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: [
+ 'form',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ options: [
+ {
+ displayName: 'Sort',
+ name: 'sort',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: false,
+ },
+ default: '',
+ placeholder: 'Add Sort',
+ options: [
+ {
+ displayName: 'Sort',
+ name: 'value',
+ values: [
+ {
+ displayName: 'Descending',
+ name: 'descending',
+ type: 'boolean',
+ default: true,
+ description: 'Sort by descending order',
+ },
+ {
+ displayName: 'Order By',
+ name: 'ordering',
+ type: 'options',
+ required: false,
+ default: 'date_modified',
+ options: [
+ {
+ name: 'Asset Type',
+ value: 'asset_type',
+ },
+ {
+ name: 'Date Modified',
+ value: 'date_modified',
+ },
+ {
+ name: 'Name',
+ value: 'name',
+ },
+ {
+ name: 'Owner Username',
+ value: 'owner__username',
+ },
+ {
+ name: 'Subscribers Count',
+ value: 'subscribers_count',
+ },
+ ],
+ description: 'Field to order by',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Filters',
+ name: 'filters',
+ type: 'collection',
+ placeholder: 'Add Filter',
+ default: {},
+ displayOptions: {
+ show: {
+ resource: [
+ 'form',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ options: [
+ {
+ displayName: 'Filter',
+ name: 'filter',
+ type: 'string',
+ default: 'asset_type:survey',
+ required: false,
+ description: 'A text search query based on form data - e.g. "owner__username:meg AND name__icontains:quixotic" - see docs for more details',
+ },
+ ],
+ },
+];
diff --git a/packages/nodes-base/nodes/KoBoToolbox/GenericFunctions.ts b/packages/nodes-base/nodes/KoBoToolbox/GenericFunctions.ts
new file mode 100644
index 0000000000..db5fe25363
--- /dev/null
+++ b/packages/nodes-base/nodes/KoBoToolbox/GenericFunctions.ts
@@ -0,0 +1,238 @@
+import {
+ IExecuteFunctions,
+ ILoadOptionsFunctions,
+} from 'n8n-core';
+
+import {
+ IDataObject,
+ IHookFunctions,
+ IHttpRequestOptions,
+ INodeExecutionData,
+ INodePropertyOptions,
+ IWebhookFunctions,
+} from 'n8n-workflow';
+
+import * as _ from 'lodash';
+
+export async function koBoToolboxApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, option: IDataObject = {}): Promise { // tslint:disable-line:no-any
+ const credentials = await this.getCredentials('koBoToolboxApi') as IDataObject;
+
+ // Set up pagination / scrolling
+ const returnAll = !!option.returnAll;
+ if (returnAll) {
+ // Override manual pagination options
+ _.set(option, 'qs.limit', 3000);
+ // Don't pass this custom param to helpers.httpRequest
+ delete option.returnAll;
+ }
+
+ const options: IHttpRequestOptions = {
+ url: '',
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Token ${credentials.token}`,
+ },
+ json: true,
+ };
+ if (Object.keys(option)) {
+ Object.assign(options, option);
+ }
+ if (options.url && !/^http(s)?:/.test(options.url)) {
+ options.url = credentials.URL + options.url;
+ }
+
+ let results = null;
+ let keepLooking = true;
+ while (keepLooking) {
+ const response = await this.helpers.httpRequest(options);
+ // Append or set results
+ results = response.results ? _.concat(results || [], response.results) : response;
+ if (returnAll && response.next) {
+ options.url = response.next;
+ continue;
+ }
+ else {
+ keepLooking = false;
+ }
+ }
+
+ return results;
+}
+
+function parseGeoPoint(geoPoint: string): null | number[] {
+ // Check if it looks like a "lat lon z precision" flat string e.g. "-1.931161 30.079811 0 0" (lat, lon, elevation, precision)
+ const coordinates = _.split(geoPoint, ' ');
+ if (coordinates.length >= 2 && _.every(coordinates, coord => coord && /^-?\d+(?:\.\d+)?$/.test(_.toString(coord)))) {
+ // NOTE: GeoJSON uses lon, lat, while most common systems use lat, lon order!
+ return _.concat([
+ _.toNumber(coordinates[1]),
+ _.toNumber(coordinates[0]),
+ ], _.toNumber(coordinates[2]) ? _.toNumber(coordinates[2]) : []);
+ }
+ return null;
+}
+
+export function parseStringList(value: string): string[] {
+ return _.split(_.toString(value), /[\s,]+/);
+}
+
+const matchWildcard = (value: string, pattern: string): boolean => {
+ const regex = new RegExp(`^${_.escapeRegExp(pattern).replace('\\*', '.*')}$`);
+ return regex.test(value);
+};
+
+const formatValue = (value: any, format: string): any => { //tslint:disable-line:no-any
+ if (_.isString(value)) {
+ // Sanitize value
+ value = _.toString(value);
+
+ // Parse geoPoints
+ const geoPoint = parseGeoPoint(value);
+ if (geoPoint) {
+ return {
+ type: 'Point',
+ coordinates: geoPoint,
+ };
+ }
+
+ // Check if it's a closed polygon geo-shape: -1.954117 30.085159 0 0;-1.955005 30.084622 0 0;-1.956057 30.08506 0 0;-1.956393 30.086229 0 0;-1.955853 30.087143 0 0;-1.954609 30.08725 0 0;-1.953966 30.086735 0 0;-1.953805 30.085897 0 0;-1.954117 30.085159 0 0
+ const points = value.split(';');
+ if (points.length >= 2 && /^[-\d\.\s;]+$/.test(value)) {
+ // Using the GeoJSON format as per https://geojson.org/
+ const coordinates = _.compact(points.map(parseGeoPoint));
+ // Only return if all values are properly parsed
+ if (coordinates.length === points.length) {
+ return {
+ type: _.first(points) === _.last(points) ? 'Polygon' : 'LineString', // check if shape is closed or open
+ coordinates,
+ };
+ }
+ }
+
+ // Parse numbers
+ if ('number' === format) {
+ return _.toNumber(value);
+ }
+
+ // Split multi-select
+ if ('multiSelect' === format) {
+ return _.split(_.toString(value), ' ');
+ }
+ }
+
+ return value;
+};
+
+export function formatSubmission(submission: IDataObject, selectMasks: string[] = [], numberMasks: string[] = []): IDataObject {
+ // Create a shallow copy of the submission
+ const response = {} as IDataObject;
+
+ for (const key of Object.keys(submission)) {
+ let value = _.clone(submission[key]);
+ // Sanitize key names: split by group, trim _
+ const sanitizedKey = key.split('/').map(k => _.trim(k, ' _')).join('.');
+ const leafKey = sanitizedKey.split('.').pop() || '';
+ let format = 'string';
+ if (_.some(numberMasks, mask => matchWildcard(leafKey, mask))) {
+ format = 'number';
+ }
+ if (_.some(selectMasks, mask => matchWildcard(leafKey, mask))) {
+ format = 'multiSelect';
+ }
+
+ value = formatValue(value, format);
+
+ _.set(response, sanitizedKey, value);
+ }
+
+ // Reformat _geolocation
+ if (_.isArray(response.geolocation) && response.geolocation.length === 2 && response.geolocation[0] && response.geolocation[1]) {
+ response.geolocation = {
+ type: 'Point',
+ coordinates: [response.geolocation[1], response.geolocation[0]],
+ };
+ }
+
+ return response;
+}
+
+export async function downloadAttachments(this: IExecuteFunctions | IWebhookFunctions, submission: IDataObject, options: IDataObject): Promise {
+ // Initialize return object with the original submission JSON content
+ const binaryItem: INodeExecutionData = {
+ json: {
+ ...submission,
+ },
+ binary: {},
+ };
+
+ const credentials = await this.getCredentials('koBoToolboxApi') as IDataObject;
+
+ // Look for attachment links - there can be more than one
+ const attachmentList = (submission['_attachments'] || submission['attachments']) as any[]; // tslint:disable-line:no-any
+ if (attachmentList && attachmentList.length) {
+ for (const [index, attachment] of attachmentList.entries()) {
+ // look for the question name linked to this attachment
+ const filename = attachment.filename;
+ Object.keys(submission).forEach(question => {
+ if (filename.endsWith('/' + _.toString(submission[question]).replace(/\s/g, '_'))) {
+ }
+ });
+
+ // Download attachment
+ // NOTE: this needs to follow redirects (possibly across domains), while keeping Authorization headers
+ // The Axios client will not propagate the Authorization header on redirects (see https://github.com/axios/axios/issues/3607), so we need to follow ourselves...
+ let response = null;
+ const attachmentUrl = attachment[options.version as string] || attachment.download_url as string;
+ let final = false, redir = 0;
+
+ const axiosOptions: IHttpRequestOptions = {
+ url: attachmentUrl,
+ method: 'GET',
+ headers: {
+ 'Authorization': `Token ${credentials.token}`,
+ },
+ ignoreHttpStatusErrors: true,
+ returnFullResponse: true,
+ disableFollowRedirect: true,
+ encoding: 'arraybuffer',
+ };
+
+ while (!final && redir < 5) {
+ response = await this.helpers.httpRequest(axiosOptions);
+
+ if (response && response.headers.location) {
+ // Follow redirect
+ axiosOptions.url = response.headers.location;
+ redir++;
+ } else {
+ final = true;
+ }
+ }
+
+ const dataPropertyAttachmentsPrefixName = options.dataPropertyAttachmentsPrefixName || 'attachment_';
+ const fileName = filename.split('/').pop();
+
+ if (response && response.body) {
+ binaryItem.binary![`${dataPropertyAttachmentsPrefixName}${index}`] = await this.helpers.prepareBinaryData(response.body, fileName);
+ }
+ }
+ } else {
+ delete binaryItem.binary;
+ }
+
+ // Add item to final output - even if there's no attachment retrieved
+ return binaryItem;
+}
+
+export async function loadForms(this: ILoadOptionsFunctions): Promise {
+ const responseData = await koBoToolboxApiRequest.call(this, {
+ url: '/api/v2/assets/',
+ qs: {
+ q: 'asset_type:survey',
+ ordering: 'name',
+ },
+ scroll: true,
+ });
+
+ return responseData?.map((survey: any) => ({ name: survey.name, value: survey.uid })) || []; // tslint:disable-line:no-any
+}
diff --git a/packages/nodes-base/nodes/KoBoToolbox/HookDescription.ts b/packages/nodes-base/nodes/KoBoToolbox/HookDescription.ts
new file mode 100644
index 0000000000..f6416cd43b
--- /dev/null
+++ b/packages/nodes-base/nodes/KoBoToolbox/HookDescription.ts
@@ -0,0 +1,184 @@
+import {
+ INodeProperties,
+} from 'n8n-workflow';
+
+export const hookOperations: INodeProperties[] = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: [
+ 'hook',
+ ],
+ },
+ },
+ options: [
+ {
+ name: 'Get',
+ value: 'get',
+ description: 'Get a single hook definition',
+ },
+ {
+ name: 'Get All',
+ value: 'getAll',
+ description: 'List all hooks on a form',
+ },
+ {
+ name: 'Logs',
+ value: 'getLogs',
+ description: 'Get hook logs',
+ },
+ {
+ name: 'Retry All',
+ value: 'retryAll',
+ description: 'Retry all failed attempts for a given hook',
+ },
+ {
+ name: 'Retry One',
+ value: 'retryOne',
+ description: 'Retry a specific hook',
+ },
+ ],
+ default: 'getAll',
+ },
+];
+
+export const hookFields: INodeProperties[] = [
+
+ /* -------------------------------------------------------------------------- */
+ /* hook:get */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Form ID',
+ name: 'formId',
+ type: 'options',
+ typeOptions: {
+ loadOptionsMethod: 'loadForms',
+ },
+ required: true,
+ default: '',
+ displayOptions: {
+ show: {
+ resource: [
+ 'hook',
+ ],
+ operation: [
+ 'get',
+ 'retryOne',
+ 'retryAll',
+ 'getLogs',
+ ],
+ },
+ },
+ description: 'Form ID (e.g. aSAvYreNzVEkrWg5Gdcvg)',
+ },
+ {
+ displayName: 'Hook ID',
+ name: 'hookId',
+ type: 'string',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'hook',
+ ],
+ operation: [
+ 'get',
+ 'retryOne',
+ 'retryAll',
+ 'getLogs',
+ ],
+ },
+ },
+ default: '',
+ description: 'Hook ID (starts with h, e.g. hVehywQ2oXPYGHJHKtqth4)',
+ },
+ /* -------------------------------------------------------------------------- */
+ /* hook:getAll */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Form ID',
+ name: 'formId',
+ type: 'options',
+ typeOptions: {
+ loadOptionsMethod: 'loadForms',
+ },
+ required: true,
+ default: '',
+ displayOptions: {
+ show: {
+ resource: [
+ 'hook',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ description: 'Form ID (e.g. aSAvYreNzVEkrWg5Gdcvg)',
+ },
+ {
+ displayName: 'Hook Log ID',
+ name: 'logId',
+ type: 'string',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'hook',
+ ],
+ operation: [
+ 'retryOne',
+ ],
+ },
+ },
+ default: '',
+ description: 'Hook log ID (starts with hl, e.g. hlSbGKaUKzTVNoWEVMYbLHe)',
+ },
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ required: true,
+ default: false,
+ displayOptions: {
+ show: {
+ resource: [
+ 'hook',
+ ],
+ operation: [
+ 'getAll',
+ 'getLogs',
+ ],
+ },
+ },
+ description: 'Whether to return all results',
+ },
+ {
+ displayName: 'Limit',
+ name: 'limit',
+ type: 'number',
+ required: false,
+ typeOptions: {
+ maxValue: 3000,
+ },
+ displayOptions: {
+ show: {
+ resource: [
+ 'hook',
+ ],
+ operation: [
+ 'getAll',
+ 'getLogs',
+ ],
+ returnAll: [
+ false,
+ ],
+ },
+ },
+ default: 1000,
+ description: 'The number of results to return',
+ },
+];
diff --git a/packages/nodes-base/nodes/KoBoToolbox/KoBoToolbox.node.ts b/packages/nodes-base/nodes/KoBoToolbox/KoBoToolbox.node.ts
new file mode 100644
index 0000000000..0410da5ca1
--- /dev/null
+++ b/packages/nodes-base/nodes/KoBoToolbox/KoBoToolbox.node.ts
@@ -0,0 +1,371 @@
+import {
+ IExecuteFunctions,
+} from 'n8n-core';
+
+import {
+ ICredentialsDecrypted,
+ ICredentialTestFunctions,
+ IDataObject,
+ INodeCredentialTestResult,
+ INodeExecutionData,
+ INodeType,
+ INodeTypeDescription,
+ JsonObject,
+} from 'n8n-workflow';
+
+import {
+ downloadAttachments,
+ formatSubmission,
+ koBoToolboxApiRequest,
+ loadForms,
+ parseStringList,
+} from './GenericFunctions';
+
+import {
+ formFields,
+ formOperations
+} from './FormDescription';
+
+import {
+ submissionFields,
+ submissionOperations,
+} from './SubmissionDescription';
+
+import {
+ hookFields,
+ hookOperations,
+} from './HookDescription';
+
+export class KoBoToolbox implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'KoBoToolbox',
+ name: 'koBoToolbox',
+ icon: 'file:koBoToolbox.svg',
+ group: ['transform'],
+ version: 1,
+ description: 'Work with KoBoToolbox forms and submissions',
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
+ defaults: {
+ name: 'KoBoToolbox',
+ color: '#64C0FF',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'koBoToolboxApi',
+ required: true,
+ testedBy: 'koBoToolboxApiCredentialTest',
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ options: [
+ {
+ name: 'Form',
+ value: 'form',
+ },
+ {
+ name: 'Hook',
+ value: 'hook',
+ },
+ {
+ name: 'Submission',
+ value: 'submission',
+ },
+ ],
+ default: 'submission',
+ required: true,
+ },
+ ...formOperations,
+ ...formFields,
+ ...hookOperations,
+ ...hookFields,
+ ...submissionOperations,
+ ...submissionFields,
+ ],
+ };
+
+ methods = {
+ credentialTest: {
+ async koBoToolboxApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise {
+ const credentials = credential.data;
+ try {
+ const response = await this.helpers.request({
+ url: `${credentials!.URL}/api/v2/assets/hash`,
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Token ${credentials!.token}`,
+ },
+ json: true,
+ });
+
+ if (response.hash) {
+ return {
+ status: 'OK',
+ message: 'Connection successful!',
+ };
+ }
+ else {
+ return {
+ status: 'Error',
+ message: `Credentials are not valid. Response: ${response.detail}`,
+ };
+ }
+ }
+ catch (err) {
+ return {
+ status: 'Error',
+ message: `Credentials validation failed: ${(err as JsonObject).message}`,
+ };
+ }
+ },
+ },
+
+ loadOptions: {
+ loadForms,
+ },
+ };
+
+ async execute(this: IExecuteFunctions): Promise {
+ // tslint:disable-next-line:no-any
+ let responseData: any;
+ // tslint:disable-next-line:no-any
+ let returnData: any[] = [];
+ const binaryItems: INodeExecutionData[] = [];
+ const items = this.getInputData();
+ const resource = this.getNodeParameter('resource', 0) as string;
+ const operation = this.getNodeParameter('operation', 0) as string;
+
+ for (let i = 0; i < items.length; i++) {
+
+ if (resource === 'form') {
+ // *********************************************************************
+ // Form
+ // *********************************************************************
+
+ if (operation === 'get') {
+ // ----------------------------------
+ // Form: get
+ // ----------------------------------
+ const formId = this.getNodeParameter('formId', i) as string;
+ responseData = [await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}`,
+ })];
+ }
+
+ if (operation === 'getAll') {
+ // ----------------------------------
+ // Form: getAll
+ // ----------------------------------
+ const formQueryOptions = this.getNodeParameter('options', i) as {
+ sort: {
+ value: {
+ descending: boolean,
+ ordering: string,
+ }
+ }
+ };
+ const formFilterOptions = this.getNodeParameter('filters', i) as IDataObject;
+
+ responseData = await koBoToolboxApiRequest.call(this, {
+ url: '/api/v2/assets/',
+ qs: {
+ limit: this.getNodeParameter('limit', i, 1000) as number,
+ ...(formFilterOptions.filter && { q: formFilterOptions.filter }),
+ ...(formQueryOptions?.sort?.value?.ordering && { ordering: (formQueryOptions?.sort?.value?.descending ? '-' : '') + formQueryOptions?.sort?.value?.ordering }),
+ },
+ scroll: this.getNodeParameter('returnAll', i) as boolean,
+ });
+ }
+ }
+ if (resource === 'submission') {
+ // *********************************************************************
+ // Submissions
+ // *********************************************************************
+ const formId = this.getNodeParameter('formId', i) as string;
+
+ if (operation === 'getAll') {
+ // ----------------------------------
+ // Submissions: getAll
+ // ----------------------------------
+
+ const submissionQueryOptions = this.getNodeParameter('options', i) as IDataObject;
+
+ responseData = await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}/data/`,
+ qs: {
+ limit: this.getNodeParameter('limit', i, 1000) as number,
+ ...(submissionQueryOptions.query && { query: submissionQueryOptions.query }),
+ //...(submissionQueryOptions.sort && { sort: submissionQueryOptions.sort }),
+ ...(submissionQueryOptions.fields && { fields: JSON.stringify(parseStringList(submissionQueryOptions.fields as string)) }),
+ },
+ scroll: this.getNodeParameter('returnAll', i) as boolean,
+ });
+
+ if (submissionQueryOptions.reformat) {
+ responseData = responseData.map((submission: IDataObject) => {
+ return formatSubmission(submission, parseStringList(submissionQueryOptions.selectMask as string), parseStringList(submissionQueryOptions.numberMask as string));
+ });
+ }
+
+ if (submissionQueryOptions.download) {
+ // Download related attachments
+ for (const submission of responseData) {
+ binaryItems.push(await downloadAttachments.call(this, submission, submissionQueryOptions));
+ }
+ }
+ }
+
+ if (operation === 'get') {
+ // ----------------------------------
+ // Submissions: get
+ // ----------------------------------
+ const submissionId = this.getNodeParameter('submissionId', i) as string;
+ const options = this.getNodeParameter('options', i) as IDataObject;
+
+ responseData = [await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}/data/${submissionId}`,
+ qs: {
+ ...(options.fields && { fields: JSON.stringify(parseStringList(options.fields as string)) }),
+ },
+ })];
+
+ if (options.reformat) {
+ responseData = responseData.map((submission: IDataObject) => {
+ return formatSubmission(submission, parseStringList(options.selectMask as string), parseStringList(options.numberMask as string));
+ });
+ }
+
+ if (options.download) {
+ // Download related attachments
+ for (const submission of responseData) {
+ binaryItems.push(await downloadAttachments.call(this, submission, options));
+ }
+ }
+ }
+
+ if (operation === 'delete') {
+ // ----------------------------------
+ // Submissions: delete
+ // ----------------------------------
+ const id = this.getNodeParameter('submissionId', i) as string;
+
+ await koBoToolboxApiRequest.call(this, {
+ method: 'DELETE',
+ url: `/api/v2/assets/${formId}/data/${id}`,
+ });
+
+ responseData = [{
+ success: true,
+ }];
+ }
+
+ if (operation === 'getValidation') {
+ // ----------------------------------
+ // Submissions: getValidation
+ // ----------------------------------
+ const submissionId = this.getNodeParameter('submissionId', i) as string;
+
+ responseData = [await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}/data/${submissionId}/validation_status/`,
+ })];
+ }
+
+ if (operation === 'setValidation') {
+ // ----------------------------------
+ // Submissions: setValidation
+ // ----------------------------------
+ const submissionId = this.getNodeParameter('submissionId', i) as string;
+ const status = this.getNodeParameter('validationStatus', i) as string;
+
+ responseData = [await koBoToolboxApiRequest.call(this, {
+ method: 'PATCH',
+ url: `/api/v2/assets/${formId}/data/${submissionId}/validation_status/`,
+ body: {
+ 'validation_status.uid': status,
+ },
+ })];
+ }
+ }
+
+ if (resource === 'hook') {
+ const formId = this.getNodeParameter('formId', i) as string;
+ // *********************************************************************
+ // Hook
+ // *********************************************************************
+
+ if (operation === 'getAll') {
+ // ----------------------------------
+ // Hook: getAll
+ // ----------------------------------
+ responseData = await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}/hooks/`,
+ qs: {
+ limit: this.getNodeParameter('limit', i, 1000) as number,
+ },
+ scroll: this.getNodeParameter('returnAll', i) as boolean,
+ });
+ }
+
+ if (operation === 'get') {
+ // ----------------------------------
+ // Hook: get
+ // ----------------------------------
+ const hookId = this.getNodeParameter('hookId', i) as string;
+ responseData = [await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}/hooks/${hookId}`,
+ })];
+ }
+
+ if (operation === 'retryAll') {
+ // ----------------------------------
+ // Hook: retryAll
+ // ----------------------------------
+ const hookId = this.getNodeParameter('hookId', i) as string;
+ responseData = [await koBoToolboxApiRequest.call(this, {
+ method: 'PATCH',
+ url: `/api/v2/assets/${formId}/hooks/${hookId}/retry/`,
+ })];
+ }
+
+ if (operation === 'getLogs') {
+ // ----------------------------------
+ // Hook: getLogs
+ // ----------------------------------
+ const hookId = this.getNodeParameter('hookId', i) as string;
+ responseData = await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}/hooks/${hookId}/logs/`,
+ qs: {
+ start: this.getNodeParameter('start', i, 0) as number,
+ limit: this.getNodeParameter('limit', i, 1000) as number,
+ },
+ scroll: this.getNodeParameter('returnAll', i) as boolean,
+ });
+ }
+
+ if (operation === 'retryOne') {
+ // ----------------------------------
+ // Hook: retryOne
+ // ----------------------------------
+ const hookId = this.getNodeParameter('hookId', i) as string;
+ const logId = this.getNodeParameter('logId', i) as string;
+
+ responseData = [await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}/hooks/${hookId}/logs/${logId}/retry/`,
+ })];
+ }
+ }
+
+ returnData = returnData.concat(responseData);
+ }
+
+ // Map data to n8n data
+ return binaryItems.length > 0
+ ? [binaryItems]
+ : [this.helpers.returnJsonArray(returnData)];
+ }
+}
diff --git a/packages/nodes-base/nodes/KoBoToolbox/KoBoToolboxTrigger.node.ts b/packages/nodes-base/nodes/KoBoToolbox/KoBoToolboxTrigger.node.ts
new file mode 100644
index 0000000000..ed97f2ce3d
--- /dev/null
+++ b/packages/nodes-base/nodes/KoBoToolbox/KoBoToolboxTrigger.node.ts
@@ -0,0 +1,168 @@
+import {
+ IDataObject,
+ IHookFunctions,
+ INodeType,
+ INodeTypeDescription,
+ IWebhookFunctions,
+ IWebhookResponseData,
+} from 'n8n-workflow';
+
+import {
+ downloadAttachments,
+ formatSubmission,
+ koBoToolboxApiRequest,
+ loadForms,
+ parseStringList
+} from './GenericFunctions';
+
+import {
+ options,
+} from './Options';
+
+export class KoBoToolboxTrigger implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'KoBoToolbox Trigger',
+ name: 'koBoToolboxTrigger',
+ icon: 'file:koBoToolbox.svg',
+ group: ['trigger'],
+ version: 1,
+ description: 'Process KoBoToolbox submissions',
+ defaults: {
+ name: 'KoBoToolbox Trigger',
+ color: '#64C0FF',
+ },
+ inputs: [],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'koBoToolboxApi',
+ required: true,
+ },
+ ],
+ webhooks: [
+ {
+ name: 'default',
+ httpMethod: 'POST',
+ responseMode: 'onReceived',
+ path: 'webhook',
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Form Name/ID',
+ name: 'formId',
+ type: 'options',
+ typeOptions: {
+ loadOptionsMethod: 'loadForms',
+ },
+ required: true,
+ default: '',
+ description: 'Form ID (e.g. aSAvYreNzVEkrWg5Gdcvg)',
+ },
+ {
+ displayName: 'Trigger On',
+ name: 'triggerOn',
+ type: 'options',
+ required: true,
+ default: 'formSubmission',
+ options: [
+ {
+ name: 'On Form Submission',
+ value: 'formSubmission',
+ },
+ ],
+ },
+ { ...options },
+ ],
+ };
+
+ // @ts-ignore
+ webhookMethods = {
+ default: {
+ async checkExists(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ const webhookUrl = this.getNodeWebhookUrl('default');
+ const formId = this.getNodeParameter('formId') as string; //tslint:disable-line:variable-name
+ const webhooks = await koBoToolboxApiRequest.call(this, {
+ url: `/api/v2/assets/${formId}/hooks/`,
+ });
+ for (const webhook of webhooks || []) {
+ if (webhook.endpoint === webhookUrl && webhook.active === true) {
+ webhookData.webhookId = webhook.uid;
+ return true;
+ }
+ }
+ return false;
+ },
+
+ async create(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ const webhookUrl = this.getNodeWebhookUrl('default');
+ const formId = this.getNodeParameter('formId') as string; //tslint:disable-line:variable-name
+
+ const response = await koBoToolboxApiRequest.call(this, {
+ method: 'POST',
+ url: `/api/v2/assets/${formId}/hooks/`,
+ body: {
+ name: `n8n-webhook:${webhookUrl}`,
+ endpoint: webhookUrl,
+ email_notification: true,
+ },
+ });
+
+ if (response.uid) {
+ webhookData.webhookId = response.uid;
+ return true;
+ }
+
+ return false;
+ },
+
+ async delete(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ const formId = this.getNodeParameter('formId') as string; //tslint:disable-line:variable-name
+ try {
+ await koBoToolboxApiRequest.call(this, {
+ method: 'DELETE',
+ url: `/api/v2/assets/${formId}/hooks/${webhookData.webhookId}`,
+ });
+ } catch (error) {
+ return false;
+ }
+ delete webhookData.webhookId;
+ return true;
+ },
+ },
+ };
+
+ methods = {
+ loadOptions: {
+ loadForms,
+ },
+ };
+
+ async webhook(this: IWebhookFunctions): Promise {
+ const req = this.getRequestObject();
+ const formatOptions = this.getNodeParameter('formatOptions') as IDataObject;
+
+ const responseData = formatOptions.reformat
+ ? formatSubmission(req.body, parseStringList(formatOptions.selectMask as string), parseStringList(formatOptions.numberMask as string))
+ : req.body;
+
+ if (formatOptions.download) {
+ // Download related attachments
+ return {
+ workflowData: [
+ [await downloadAttachments.call(this, responseData, formatOptions)],
+ ],
+ };
+ }
+ else {
+ return {
+ workflowData: [
+ this.helpers.returnJsonArray([responseData]),
+ ],
+ };
+ }
+ }
+}
diff --git a/packages/nodes-base/nodes/KoBoToolbox/Options.ts b/packages/nodes-base/nodes/KoBoToolbox/Options.ts
new file mode 100644
index 0000000000..2824225e57
--- /dev/null
+++ b/packages/nodes-base/nodes/KoBoToolbox/Options.ts
@@ -0,0 +1,87 @@
+import {
+ INodeProperties,
+} from 'n8n-workflow';
+
+export const options = {
+ displayName: 'Options',
+ placeholder: 'Add Option',
+ name: 'formatOptions',
+ type: 'collection',
+ default: {},
+ options: [
+ {
+ displayName: 'Attachments Prefix',
+ name: 'dataPropertyAttachmentsPrefixName',
+ type: 'string',
+ displayOptions: {
+ show: {
+ download: [
+ true,
+ ],
+ },
+ },
+ default: 'attachment_',
+ description: 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
+ },
+ {
+ displayName: 'Download Attachments',
+ name: 'download',
+ type: 'boolean',
+ default: false,
+ description: 'Download submitted attachments',
+ },
+ {
+ displayName: 'File Size',
+ name: 'version',
+ type: 'options',
+ displayOptions: {
+ show: {
+ download: [
+ true,
+ ],
+ },
+ },
+ default: 'download_url',
+ description: 'Attachment size to retrieve, if multiple versions are available',
+ options: [
+ {
+ name: 'Original',
+ value: 'download_url',
+ },
+ {
+ name: 'Small',
+ value: 'download_small_url',
+ },
+ {
+ name: 'Medium',
+ value: 'download_medium_url',
+ },
+ {
+ name: 'Large',
+ value: 'download_large_url',
+ },
+ ],
+ },
+ {
+ displayName: 'Multiselect Mask',
+ name: 'selectMask',
+ type: 'string',
+ default: 'select_*',
+ description: 'Comma-separated list of wildcard-style selectors for fields that should be treated as multiselect fields, i.e. parsed as arrays',
+ },
+ {
+ displayName: 'Number Mask',
+ name: 'numberMask',
+ type: 'string',
+ default: 'n_*, f_*',
+ description: 'Comma-separated list of wildcard-style selectors for fields that should be treated as numbers',
+ },
+ {
+ displayName: 'Reformat',
+ name: 'reformat',
+ type: 'boolean',
+ default: false,
+ description: 'Apply some reformatting to the submission data, such as parsing GeoJSON coordinates',
+ },
+ ],
+} as INodeProperties;
diff --git a/packages/nodes-base/nodes/KoBoToolbox/SubmissionDescription.ts b/packages/nodes-base/nodes/KoBoToolbox/SubmissionDescription.ts
new file mode 100644
index 0000000000..8caaa3e7b2
--- /dev/null
+++ b/packages/nodes-base/nodes/KoBoToolbox/SubmissionDescription.ts
@@ -0,0 +1,304 @@
+import {
+ INodeProperties,
+} from 'n8n-workflow';
+
+export const submissionOperations: INodeProperties[] = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: [
+ 'submission',
+ ],
+ },
+ },
+ options: [
+ {
+ name: 'Delete',
+ value: 'delete',
+ description: 'Delete a single submission',
+ },
+ {
+ name: 'Get',
+ value: 'get',
+ description: 'Get a single submission',
+ },
+ {
+ name: 'Get All',
+ value: 'getAll',
+ description: 'Get all submissions',
+ },
+ {
+ name: 'Get Validation Status',
+ value: 'getValidation',
+ description: 'Get the validation status for the submission',
+ },
+ {
+ name: 'Update Validation Status',
+ value: 'setValidation',
+ description: 'Set the validation status of the submission',
+ },
+ ],
+ default: 'getAll',
+ },
+];
+
+export const submissionFields: INodeProperties[] = [
+
+ /* -------------------------------------------------------------------------- */
+ /* submission:get */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Form ID',
+ name: 'formId',
+ type: 'options',
+ typeOptions: {
+ loadOptionsMethod: 'loadForms',
+ },
+ required: true,
+ default: '',
+ displayOptions: {
+ show: {
+ resource: [
+ 'submission',
+ ],
+ operation: [
+ 'get',
+ 'delete',
+ 'getValidation',
+ 'setValidation',
+ ],
+ },
+ },
+ description: 'Form ID (e.g. aSAvYreNzVEkrWg5Gdcvg)',
+ },
+ {
+ displayName: 'Submission ID',
+ name: 'submissionId',
+ type: 'string',
+ required: true,
+ default: '',
+ displayOptions: {
+ show: {
+ resource: [
+ 'submission',
+ ],
+ operation: [
+ 'get',
+ 'delete',
+ 'getValidation',
+ 'setValidation',
+ ],
+ },
+ },
+ description: 'Submission ID (number, e.g. 245128)',
+ },
+ {
+ displayName: 'Validation Status',
+ name: 'validationStatus',
+ type: 'options',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: [
+ 'submission',
+ ],
+ operation: [
+ 'setValidation',
+ ],
+ },
+ },
+ default: '',
+ options: [
+ {
+ name: 'Approved',
+ value: 'validation_status_approved',
+ },
+ {
+ name: 'Not Approved',
+ value: 'validation_status_not_approved',
+ },
+ {
+ name: 'On Hold',
+ value: 'validation_status_on_hold',
+ },
+ ],
+ description: 'Desired Validation Status',
+ },
+ /* -------------------------------------------------------------------------- */
+ /* submission:getAll */
+ /* -------------------------------------------------------------------------- */
+ {
+ displayName: 'Form Name/ID',
+ name: 'formId',
+ type: 'options',
+ typeOptions: {
+ loadOptionsMethod: 'loadForms',
+ },
+ required: true,
+ default: '',
+ displayOptions: {
+ show: {
+ resource: [
+ 'submission',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ description: 'Form ID (e.g. aSAvYreNzVEkrWg5Gdcvg)',
+ },
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ required: true,
+ default: false,
+ displayOptions: {
+ show: {
+ resource: [
+ 'submission',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ },
+ },
+ description: 'Whether to return all results',
+ },
+ {
+ displayName: 'Limit',
+ name: 'limit',
+ type: 'number',
+ required: false,
+ typeOptions: {
+ maxValue: 3000,
+ },
+ displayOptions: {
+ show: {
+ resource: [
+ 'submission',
+ ],
+ operation: [
+ 'getAll',
+ ],
+ returnAll: [
+ false,
+ ],
+ },
+ },
+ default: 100,
+ description: 'The number of results to return',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ displayOptions: {
+ show: {
+ resource: [
+ 'submission',
+ ],
+ operation: [
+ 'get',
+ 'getAll',
+ ],
+ },
+ },
+ default: {},
+ placeholder: 'Add Option',
+ options: [
+ {
+ displayName: 'Attachments Prefix',
+ name: 'dataPropertyAttachmentsPrefixName',
+ type: 'string',
+ displayOptions: {
+ show: {
+ download: [
+ true,
+ ],
+ },
+ },
+ default: 'attachment_',
+ description: 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"',
+ },
+ {
+ displayName: 'Download Attachments',
+ name: 'download',
+ type: 'boolean',
+ default: false,
+ description: 'Download submitted attachments',
+ },
+ {
+ displayName: 'Fields to Retrieve',
+ name: 'fields',
+ type: 'string',
+ default: '',
+ description: 'Comma-separated list of fields to retrieve (e.g. _submission_time,_submitted_by). If left blank, all fields are retrieved',
+ },
+ {
+ displayName: 'File Size',
+ name: 'version',
+ type: 'options',
+ displayOptions: {
+ show: {
+ download: [
+ true,
+ ],
+ },
+ },
+ default: 'download_url',
+ description: 'Attachment size to retrieve, if multiple versions are available',
+ options: [
+ {
+ name: 'Original',
+ value: 'download_url',
+ },
+ {
+ name: 'Small',
+ value: 'download_small_url',
+ },
+ {
+ name: 'Medium',
+ value: 'download_medium_url',
+ },
+ {
+ name: 'Large',
+ value: 'download_large_url',
+ },
+ ],
+ },
+ {
+ displayName: 'Multiselect Mask',
+ name: 'selectMask',
+ type: 'string',
+ default: 'select_*',
+ description: 'Comma-separated list of wildcard-style selectors for fields that should be treated as multiselect fields, i.e. parsed as arrays',
+ },
+ {
+ displayName: 'Number Mask',
+ name: 'numberMask',
+ type: 'string',
+ default: 'n_*, f_*',
+ description: 'Comma-separated list of wildcard-style selectors for fields that should be treated as numbers',
+ },
+ {
+ displayName: 'Reformat',
+ name: 'reformat',
+ type: 'boolean',
+ default: false,
+ description: 'Apply some reformatting to the submission data, such as parsing GeoJSON coordinates',
+ },
+ // {
+ // displayName: 'Sort',
+ // name: 'sort',
+ // type: 'json',
+ // default: '',
+ // description: 'Sort predicates, in Mongo JSON format (e.g. {"_submission_time":1})',
+ // },
+ ],
+ },
+];
diff --git a/packages/nodes-base/nodes/KoBoToolbox/koBoToolbox.svg b/packages/nodes-base/nodes/KoBoToolbox/koBoToolbox.svg
new file mode 100644
index 0000000000..a26f0eb303
--- /dev/null
+++ b/packages/nodes-base/nodes/KoBoToolbox/koBoToolbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index be20e4d888..84f98314c9 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -159,6 +159,7 @@
"dist/credentials/Kafka.credentials.js",
"dist/credentials/KeapOAuth2Api.credentials.js",
"dist/credentials/KitemakerApi.credentials.js",
+ "dist/credentials/KoBoToolboxApi.credentials.js",
"dist/credentials/LemlistApi.credentials.js",
"dist/credentials/LinearApi.credentials.js",
"dist/credentials/LineNotifyOAuth2Api.credentials.js",
@@ -493,6 +494,8 @@
"dist/nodes/Keap/Keap.node.js",
"dist/nodes/Keap/KeapTrigger.node.js",
"dist/nodes/Kitemaker/Kitemaker.node.js",
+ "dist/nodes/KoBoToolbox/KoBoToolbox.node.js",
+ "dist/nodes/KoBoToolbox/KoBoToolboxTrigger.node.js",
"dist/nodes/Lemlist/Lemlist.node.js",
"dist/nodes/Lemlist/LemlistTrigger.node.js",
"dist/nodes/Line/Line.node.js",
@@ -787,4 +790,4 @@
"json"
]
}
-}
+}
\ No newline at end of file