Merge branch 'master' into feature/wordpress-node

This commit is contained in:
Ricardo Espinoza
2019-12-30 16:08:43 -05:00
32 changed files with 498 additions and 127 deletions

27
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Node CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
npm install
npm run bootstrap
npm run build --if-present
npm test
env:
CI: true

View File

@@ -100,11 +100,13 @@ export N8N_CUSTOM_EXTENSIONS="/home/jim/n8n/custom-nodes;/data/n8n/nodes"
``` ```
## Use built-in modules in Function-Nodes ## Use built-in and external modules in Function-Nodes
By default is it for security reasons not allowed to import modules in Function-Nodes. For security reasons, importing modules is restricted by default in Function-Nodes.
It is, however, possible to lift that restriction for built-in modules by setting the It is, however, possible to lift that restriction for built-in and external modules by
environment variable `NODE_FUNCTION_ALLOW_BUILTIN`. setting the following environment variables:
`NODE_FUNCTION_ALLOW_BUILTIN`: For builtin modules
`NODE_FUNCTION_ALLOW_EXTERNAL`: For external modules sourced from n8n/node_modules directory. External module support is disabled when env variable is not set.
```bash ```bash
# Allows usage of all builtin modules # Allows usage of all builtin modules
@@ -115,6 +117,9 @@ export NODE_FUNCTION_ALLOW_BUILTIN=crypto
# Allows usage of only crypto and fs # Allows usage of only crypto and fs
export NODE_FUNCTION_ALLOW_BUILTIN=crypto,fs export NODE_FUNCTION_ALLOW_BUILTIN=crypto,fs
# Allow usage of external npm modules. Wildcard matching is not supported.
export NODE_FUNCTION_ALLOW_EXTERNAL=moment,lodash
``` ```

View File

@@ -5,8 +5,7 @@ import {
} from "n8n-core"; } from "n8n-core";
import { Command, flags } from '@oclif/command'; import { Command, flags } from '@oclif/command';
const open = require('open'); const open = require('open');
import { promisify } from 'util'; // import { dirname } from 'path';
import { dirname } from 'path';
import * as config from '../config'; import * as config from '../config';
import { import {
@@ -20,7 +19,6 @@ import {
TestWebhooks, TestWebhooks,
} from "../src"; } from "../src";
const tunnel = promisify(localtunnel);
// // Add support for internationalization // // Add support for internationalization
// const fullIcuPath = require.resolve('full-icu'); // const fullIcuPath = require.resolve('full-icu');
@@ -151,7 +149,7 @@ export class Start extends Command {
const port = config.get('port') as number; const port = config.get('port') as number;
// @ts-ignore // @ts-ignore
const webhookTunnel = await tunnel(port, tunnelSettings); const webhookTunnel = await localtunnel(port, tunnelSettings);
process.env.WEBHOOK_TUNNEL_URL = webhookTunnel.url + '/'; process.env.WEBHOOK_TUNNEL_URL = webhookTunnel.url + '/';
this.log(`Tunnel URL: ${process.env.WEBHOOK_TUNNEL_URL}\n`); this.log(`Tunnel URL: ${process.env.WEBHOOK_TUNNEL_URL}\n`);

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.40.0", "version": "0.41.0",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@@ -52,24 +52,24 @@
"devDependencies": { "devDependencies": {
"@oclif/dev-cli": "^1.22.2", "@oclif/dev-cli": "^1.22.2",
"@types/basic-auth": "^1.1.2", "@types/basic-auth": "^1.1.2",
"@types/compression": "0.0.36", "@types/compression": "1.0.1",
"@types/connect-history-api-fallback": "^1.3.1", "@types/connect-history-api-fallback": "^1.3.1",
"@types/convict": "^4.2.1", "@types/convict": "^4.2.1",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^8.2.0",
"@types/express": "^4.16.1", "@types/express": "^4.16.1",
"@types/jest": "^24.0.18", "@types/jest": "^24.0.18",
"@types/localtunnel": "^1.9.0", "@types/localtunnel": "^1.9.0",
"@types/lodash.get": "^4.4.2", "@types/lodash.get": "^4.4.6",
"@types/node": "^10.10.1", "@types/node": "^10.10.1",
"@types/open": "^6.1.0", "@types/open": "^6.1.0",
"@types/parseurl": "^1.3.1", "@types/parseurl": "^1.3.1",
"@types/request-promise-native": "^1.0.15", "@types/request-promise-native": "^1.0.15",
"jest": "^24.9.0", "jest": "^24.9.0",
"nodemon": "^1.19.1", "nodemon": "^2.0.2",
"run-script-os": "^1.0.7", "run-script-os": "^1.0.7",
"ts-jest": "^24.0.2", "ts-jest": "^24.0.2",
"tslint": "^5.17.0", "tslint": "^5.17.0",
"typescript": "~3.5.2" "typescript": "~3.7.4"
}, },
"dependencies": { "dependencies": {
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
@@ -86,17 +86,17 @@
"flatted": "^2.0.0", "flatted": "^2.0.0",
"glob-promise": "^3.4.0", "glob-promise": "^3.4.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"inquirer": "^6.5.1", "inquirer": "^7.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"jwks-rsa": "^1.6.0", "jwks-rsa": "^1.6.0",
"localtunnel": "^1.9.1", "localtunnel": "^2.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mongodb": "^3.2.3", "mongodb": "^3.2.3",
"n8n-core": "~0.18.0", "n8n-core": "~0.18.0",
"n8n-editor-ui": "~0.29.0", "n8n-editor-ui": "~0.29.0",
"n8n-nodes-base": "~0.35.0", "n8n-nodes-base": "~0.36.0",
"n8n-workflow": "~0.18.0", "n8n-workflow": "~0.18.0",
"open": "^6.1.0", "open": "^7.0.0",
"pg": "^7.11.0", "pg": "^7.11.0",
"request-promise-native": "^1.0.7", "request-promise-native": "^1.0.7",
"sqlite3": "^4.0.6", "sqlite3": "^4.0.6",

View File

@@ -28,7 +28,7 @@
"@types/crypto-js": "^3.1.43", "@types/crypto-js": "^3.1.43",
"@types/express": "^4.16.1", "@types/express": "^4.16.1",
"@types/jest": "^24.0.18", "@types/jest": "^24.0.18",
"@types/lodash.get": "^4.4.5", "@types/lodash.get": "^4.4.6",
"@types/mmmagic": "^0.4.29", "@types/mmmagic": "^0.4.29",
"@types/node": "^10.10.1", "@types/node": "^10.10.1",
"@types/request-promise-native": "^1.0.15", "@types/request-promise-native": "^1.0.15",
@@ -36,7 +36,7 @@
"source-map-support": "^0.5.9", "source-map-support": "^0.5.9",
"ts-jest": "^24.0.2", "ts-jest": "^24.0.2",
"tslint": "^5.17.0", "tslint": "^5.17.0",
"typescript": "~3.5.2" "typescript": "~3.7.4"
}, },
"dependencies": { "dependencies": {
"crypto-js": "^3.1.9-1", "crypto-js": "^3.1.9-1",

View File

@@ -5,7 +5,6 @@ module.exports = {
}, },
'extends': [ 'extends': [
'plugin:vue/essential', 'plugin:vue/essential',
'@vue/standard',
'@vue/typescript', '@vue/typescript',
], ],
rules: { rules: {
@@ -18,6 +17,6 @@ module.exports = {
'no-labels': 0, 'no-labels': 0,
}, },
parserOptions: { parserOptions: {
parser: 'typescript-eslint-parser', parser: '@typescript-eslint/parser',
}, },
}; };

View File

@@ -32,25 +32,29 @@
"@types/dateformat": "^3.0.0", "@types/dateformat": "^3.0.0",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/jest": "^24.0.18", "@types/jest": "^24.0.18",
"@types/lodash.get": "^4.4.5", "@types/lodash.get": "^4.4.6",
"@types/lodash.set": "^4.3.6", "@types/lodash.set": "^4.3.6",
"@types/node": "12.12.22",
"@types/quill": "^2.0.1", "@types/quill": "^2.0.1",
"@vue/cli-plugin-babel": "^3.8.0", "@typescript-eslint/eslint-plugin": "^2.13.0",
"@vue/cli-plugin-e2e-cypress": "^3.8.0", "@typescript-eslint/parser": "^2.13.0",
"@vue/cli-plugin-eslint": "^3.8.0", "@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-typescript": "~3.8.1", "@vue/cli-plugin-e2e-cypress": "^4.1.2",
"@vue/cli-plugin-unit-jest": "^3.8.0", "@vue/cli-plugin-eslint": "^4.1.2",
"@vue/cli-service": "^3.8.0", "@vue/cli-plugin-typescript": "~4.1.2",
"@vue/eslint-config-standard": "^4.0.0", "@vue/cli-plugin-unit-jest": "^4.1.2",
"@vue/eslint-config-typescript": "~3.2.0", "@vue/cli-service": "^4.1.2",
"@vue/test-utils": "^1.0.0-beta.20", "@vue/eslint-config-standard": "^5.0.1",
"axios": "^0.18.1", "@vue/eslint-config-typescript": "~5.0.1",
"@vue/test-utils": "^1.0.0-beta.24",
"axios": "^0.19.0",
"babel-core": "7.0.0-bridge.0", "babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"dateformat": "^3.0.3", "dateformat": "^3.0.3",
"element-ui": "~2.4.11", "element-ui": "~2.13.0",
"eslint": "^5.8.0", "eslint": "^6.8.0",
"eslint-plugin-vue": "^5.0.0-0", "eslint-plugin-import": "^2.19.1",
"eslint-plugin-vue": "^6.1.1",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"flatted": "^2.0.0", "flatted": "^2.0.0",
"jquery": "^3.4.1", "jquery": "^3.4.1",
@@ -64,18 +68,18 @@
"prismjs": "^1.17.1", "prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3", "quill": "^2.0.0-dev.3",
"quill-autoformat": "^0.1.1", "quill-autoformat": "^0.1.1",
"sass-loader": "^7.0.1", "sass-loader": "^8.0.0",
"string-template-parser": "^1.2.6", "string-template-parser": "^1.2.6",
"ts-jest": "^24.0.2", "ts-jest": "^24.0.2",
"tslint": "^5.17.0", "tslint": "^5.17.0",
"typescript": "~3.5.2", "typescript": "~3.7.4",
"vue": "^2.6.9", "vue": "^2.6.9",
"vue-cli-plugin-webpack-bundle-analyzer": "^1.3.0", "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",
"vue-json-tree": "^0.4.1", "vue-json-tree": "^0.4.1",
"vue-prism-editor": "^0.3.0", "vue-prism-editor": "^0.3.0",
"vue-router": "^3.0.6", "vue-router": "^3.0.6",
"vue-template-compiler": "^2.5.17", "vue-template-compiler": "^2.5.17",
"vue-typed-mixins": "^0.1.0", "vue-typed-mixins": "^0.2.0",
"vuex": "^3.1.1" "vuex": "^3.1.1"
} }
} }

View File

@@ -21,14 +21,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue';
import { import {
IBinaryData, IBinaryData,
IBinaryKeyData,
IRunData, IRunData,
IRunExecutionData, IRunExecutionData,
Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';

View File

@@ -37,8 +37,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue';
import { restApi } from '@/components/mixins/restApi'; import { restApi } from '@/components/mixins/restApi';
import { ICredentialsResponse } from '@/Interface'; import { ICredentialsResponse } from '@/Interface';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
@@ -72,7 +70,7 @@ export default mixins(
}; };
}, },
watch: { watch: {
dialogVisible (newValue, oldValue) { dialogVisible (newValue) {
if (newValue) { if (newValue) {
this.loadCredentials(); this.loadCredentials();
this.loadCredentialTypes(); this.loadCredentialTypes();
@@ -146,9 +144,8 @@ export default mixins(
return; return;
} }
let result;
try { try {
result = await this.restApi().deleteCredentials(credential.id!); await this.restApi().deleteCredentials(credential.id!);
} catch (error) { } catch (error) {
this.$showError(error, 'Problem deleting credentials', 'There was a problem deleting the credentials:'); this.$showError(error, 'Problem deleting credentials', 'There was a problem deleting the credentials:');
return; return;

View File

@@ -160,6 +160,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { MessageBoxInputData } from 'element-ui/types/message-box';
import { import {
IExecutionResponse, IExecutionResponse,
@@ -349,7 +350,7 @@ export default mixins(
cancelButtonText: 'Cancel', cancelButtonText: 'Cancel',
inputErrorMessage: 'Invalid URL', inputErrorMessage: 'Invalid URL',
inputPattern: /^http[s]?:\/\/.*\.json$/i, inputPattern: /^http[s]?:\/\/.*\.json$/i,
}); }) as MessageBoxInputData;
this.$root.$emit('importWorkflowUrl', { url: promptResponse.value }); this.$root.$emit('importWorkflowUrl', { url: promptResponse.value });
} catch (e) {} } catch (e) {}
@@ -361,7 +362,7 @@ export default mixins(
inputValue: this.workflowName, inputValue: this.workflowName,
confirmButtonText: 'Rename', confirmButtonText: 'Rename',
cancelButtonText: 'Cancel', cancelButtonText: 'Cancel',
} },
) )
.then((data) => { .then((data) => {
// @ts-ignore // @ts-ignore

View File

@@ -10,7 +10,7 @@
<prism-editor v-if="!codeEditDialogVisible" :lineNumbers="true" :readonly="true" :code="displayValue" language="js"></prism-editor> <prism-editor v-if="!codeEditDialogVisible" :lineNumbers="true" :readonly="true" :code="displayValue" language="js"></prism-editor>
</div> </div>
<el-input v-else ref="inputField" size="small" :type="getStringInputType" :rows="getArgument('rows')" :value="displayValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" :placeholder="isValueExpression?'':parameter.placeholder"> <el-input v-else v-model="tempValue" ref="inputField" size="small" :type="getStringInputType" :rows="getArgument('rows')" :value="displayValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" :placeholder="isValueExpression?'':parameter.placeholder">
<font-awesome-icon v-if="!isValueExpression && !isReadOnly" slot="suffix" icon="external-link-alt" class="edit-window-button clickable" title="Open Edit Window" @click="displayEditDialog()" /> <font-awesome-icon v-if="!isValueExpression && !isReadOnly" slot="suffix" icon="external-link-alt" class="edit-window-button clickable" title="Open Edit Window" @click="displayEditDialog()" />
</el-input> </el-input>
</div> </div>
@@ -83,7 +83,7 @@
<div v-else-if="parameter.type === 'color'" ref="inputField" class="color-input"> <div v-else-if="parameter.type === 'color'" ref="inputField" class="color-input">
<el-color-picker :value="displayValue" :disabled="isReadOnly" @change="valueChanged" size="small" class="color-picker" @focus="setFocus" :title="displayTitle" ></el-color-picker> <el-color-picker :value="displayValue" :disabled="isReadOnly" @change="valueChanged" size="small" class="color-picker" @focus="setFocus" :title="displayTitle" ></el-color-picker>
<el-input size="small" type="text" :value="displayValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" ></el-input> <el-input v-model="tempValue" size="small" type="text" :value="displayValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" ></el-input>
</div> </div>
<div v-else-if="parameter.type === 'boolean'"> <div v-else-if="parameter.type === 'boolean'">
@@ -173,7 +173,7 @@ export default mixins(
remoteParameterOptionsLoading: false, remoteParameterOptionsLoading: false,
remoteParameterOptionsLoadingIssues: null as string | null, remoteParameterOptionsLoadingIssues: null as string | null,
textEditDialogVisible: false, textEditDialogVisible: false,
tempValue: '', // el-date-picker does not seem to work without v-model so add one tempValue: '', // el-date-picker and el-input does not seem to work without v-model so add one
dateTimePickerOptions: { dateTimePickerOptions: {
shortcuts: [ shortcuts: [
{ {

View File

@@ -123,7 +123,7 @@ export default mixins(
}, },
getArgument ( getArgument (
argumentName: string, argumentName: string,
parameter: INodeProperties parameter: INodeProperties,
): string | number | boolean | undefined { ): string | number | boolean | undefined {
if (parameter.typeOptions === undefined) { if (parameter.typeOptions === undefined) {
return undefined; return undefined;

View File

@@ -190,7 +190,7 @@ export default mixins(
key: fullpath, key: fullpath,
allowParentSelect: true, allowParentSelect: true,
dataType: 'array', dataType: 'array',
} as IVariableSelectorOption } as IVariableSelectorOption,
); );
} else if (typeof inputData === 'object') { } else if (typeof inputData === 'object') {
const tempValue: IVariableSelectorOption[] = []; const tempValue: IVariableSelectorOption[] = [];
@@ -207,7 +207,7 @@ export default mixins(
key: fullpath, key: fullpath,
allowParentSelect: true, allowParentSelect: true,
dataType: 'object', dataType: 'object',
} as IVariableSelectorOption } as IVariableSelectorOption,
); );
} }
} else { } else {
@@ -222,7 +222,7 @@ export default mixins(
name: propertyName, name: propertyName,
key: fullpath, key: fullpath,
value: inputData, value: inputData,
} as IVariableSelectorOption } as IVariableSelectorOption,
); );
} }
} }
@@ -294,7 +294,7 @@ export default mixins(
{ {
name: 'JSON', name: 'JSON',
options: this.sortOptions(jsonDataOptions), options: this.sortOptions(jsonDataOptions),
} },
); );
} }
} }
@@ -321,7 +321,7 @@ export default mixins(
name: propertyName, name: propertyName,
key: `$node["${nodeName}"].binary.${dataPropertyName}.${propertyName}`, key: `$node["${nodeName}"].binary.${dataPropertyName}.${propertyName}`,
value: outputData.binary![dataPropertyName][propertyName], value: outputData.binary![dataPropertyName][propertyName],
} },
); );
} }
@@ -332,7 +332,7 @@ export default mixins(
key: `$node["${nodeName}"].binary.${dataPropertyName}`, key: `$node["${nodeName}"].binary.${dataPropertyName}`,
options: this.sortOptions(binaryPropertyData), options: this.sortOptions(binaryPropertyData),
allowParentSelect: true, allowParentSelect: true,
} },
); );
} }
} }
@@ -343,7 +343,7 @@ export default mixins(
key: `$node["${nodeName}"].binary`, key: `$node["${nodeName}"].binary`,
options: this.sortOptions(binaryData), options: this.sortOptions(binaryData),
allowParentSelect: true, allowParentSelect: true,
} },
); );
} }
} }
@@ -476,7 +476,7 @@ export default mixins(
{ {
name: 'Input Data', name: 'Input Data',
options: this.sortOptions(tempOutputData), options: this.sortOptions(tempOutputData),
} },
); );
} else { } else {
// Data is to large so do not add // Data is to large so do not add
@@ -488,7 +488,7 @@ export default mixins(
name: '[Data to large]', name: '[Data to large]',
}, },
], ],
} },
); );
} }
} }
@@ -504,14 +504,14 @@ export default mixins(
{ {
name: 'Parameters', name: 'Parameters',
options: this.sortOptions(this.getNodeParameters(activeNode.name, initialPath, skipParameter, filterText) as IVariableSelectorOption[]), options: this.sortOptions(this.getNodeParameters(activeNode.name, initialPath, skipParameter, filterText) as IVariableSelectorOption[]),
} },
); );
returnData.push( returnData.push(
{ {
name: 'Current Node', name: 'Current Node',
options: this.sortOptions(currentNodeData), options: this.sortOptions(currentNodeData),
} },
); );
// Add the input data // Add the input data
@@ -562,7 +562,7 @@ export default mixins(
{ {
name: 'Output Data', name: 'Output Data',
options: this.sortOptions(tempOutputData), options: this.sortOptions(tempOutputData),
} as IVariableSelectorOption } as IVariableSelectorOption,
); );
} }
} }
@@ -571,7 +571,7 @@ export default mixins(
{ {
name: nodeName, name: nodeName,
options: this.sortOptions(nodeOptions), options: this.sortOptions(nodeOptions),
} },
); );
} }
@@ -579,7 +579,7 @@ export default mixins(
{ {
name: 'Nodes', name: 'Nodes',
options: this.sortOptions(allNodesData), options: this.sortOptions(allNodesData),
} },
); );
// Remove empty entries and return // Remove empty entries and return

View File

@@ -158,7 +158,7 @@ export default mixins(
}); });
}, },
}, },
} },
); );
</script> </script>

View File

@@ -104,13 +104,13 @@ export default mixins(
workflowData.updatedAt = this.convertToDisplayDate(workflowData.updatedAt as number); workflowData.updatedAt = this.convertToDisplayDate(workflowData.updatedAt as number);
}); });
this.isDataLoading = false; this.isDataLoading = false;
} },
) )
.catch( .catch(
(error: Error) => { (error: Error) => {
this.$showError(error, 'Problem loading workflows', 'There was a problem loading the workflows:'); this.$showError(error, 'Problem loading workflows', 'There was a problem loading the workflows:');
this.isDataLoading = false; this.isDataLoading = false;
} },
); );
}, },
workflowActiveChanged (data: { id: string, active: boolean }) { workflowActiveChanged (data: { id: string, active: boolean }) {

View File

@@ -185,7 +185,7 @@ export default mixins(
key: 'none', key: 'none',
value: 'Do not save', value: 'Do not save',
}, },
] ],
); );
}, },
async loadSaveDataSuccessExecutionOptions () { async loadSaveDataSuccessExecutionOptions () {
@@ -204,7 +204,7 @@ export default mixins(
key: 'none', key: 'none',
value: 'Do not save', value: 'Do not save',
}, },
] ],
); );
}, },
async loadSaveManualOptions () { async loadSaveManualOptions () {

View File

@@ -63,7 +63,7 @@ export const genericHelpers = mixins(showMessage).extend({
text: 'Loading', text: 'Loading',
spinner: 'el-icon-loading', spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.8)', background: 'rgba(255, 255, 255, 0.8)',
} },
); );
}, },
stopLoading () { stopLoading () {

View File

@@ -122,7 +122,7 @@ export const pushConnection = mixins(
// @ts-ignore // @ts-ignore
receivedData = JSON.parse(event.data); receivedData = JSON.parse(event.data);
} catch (error) { } catch (error) {
console.error('The received push data is not valid JSON.'); console.error('The received push data is not valid JSON.'); // eslint-disable-line no-console
return; return;
} }

View File

@@ -1,7 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import { parse } from 'flatted'; import { parse } from 'flatted';
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig, Method } from 'axios';
import { import {
IActivationError, IActivationError,
ICredentialsDecryptedResponse, ICredentialsDecryptedResponse,
@@ -94,7 +94,7 @@ export const restApi = Vue.extend({
restApi (): IRestApi { restApi (): IRestApi {
const self = this; const self = this;
return { return {
async makeRestApiRequest (method: string, endpoint: string, data?: IDataObject): Promise<any> { // tslint:disable-line:no-any async makeRestApiRequest (method: Method, endpoint: string, data?: IDataObject): Promise<any> { // tslint:disable-line:no-any
try { try {
const options: AxiosRequestConfig = { const options: AxiosRequestConfig = {
method, method,

View File

@@ -370,7 +370,7 @@ export const workflowHelpers = mixins(
{ {
confirmButtonText: 'Save', confirmButtonText: 'Save',
cancelButtonText: 'Cancel', cancelButtonText: 'Cancel',
} },
) )
.then((data) => { .then((data) => {
// @ts-ignore // @ts-ignore
@@ -403,7 +403,7 @@ export const workflowHelpers = mixins(
if (currentWorkflow === undefined || withNewName === true) { if (currentWorkflow === undefined || withNewName === true) {
// Workflow is new or is supposed to get saved under a new name // Workflow is new or is supposed to get saved under a new name
// so create a new entry in database // so create a new entry in database
workflowData.name = workflowName.trim() as string; workflowData.name = workflowName!.trim() as string;
if (withNewName === true) { if (withNewName === true) {
// If an existing workflow gets resaved with a new name // If an existing workflow gets resaved with a new name

View File

@@ -29,7 +29,7 @@ export const workflowSave = mixins(
{ {
confirmButtonText: 'Save', confirmButtonText: 'Save',
cancelButtonText: 'Cancel', cancelButtonText: 'Cancel',
} },
) )
.then((data) => { .then((data) => {
// @ts-ignore // @ts-ignore
@@ -62,7 +62,7 @@ export const workflowSave = mixins(
if (currentWorkflow === undefined || withNewName === true) { if (currentWorkflow === undefined || withNewName === true) {
// Workflow is new or is supposed to get saved under a new name // Workflow is new or is supposed to get saved under a new name
// so create a new entry in database // so create a new entry in database
workflowData.name = workflowName.trim() as string; workflowData.name = workflowName!.trim() as string;
if (withNewName === true) { if (withNewName === true) {
// If an existing workflow gets resaved with a new name // If an existing workflow gets resaved with a new name

View File

@@ -34,6 +34,7 @@ import {
faClone, faClone,
faCloud, faCloud,
faCopy, faCopy,
faCut,
faDotCircle, faDotCircle,
faEdit, faEdit,
faEnvelope, faEnvelope,
@@ -106,6 +107,7 @@ library.add(faCogs);
library.add(faClone); library.add(faClone);
library.add(faCloud); library.add(faCloud);
library.add(faCopy); library.add(faCopy);
library.add(faCut);
library.add(faDotCircle); library.add(faDotCircle);
library.add(faEdit); library.add(faEdit);
library.add(faEnvelope); library.add(faEnvelope);

View File

@@ -102,6 +102,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { MessageBoxInputData } from 'element-ui/types/message-box';
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb'; import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants'; import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { copyPaste } from '@/components/mixins/copyPaste'; import { copyPaste } from '@/components/mixins/copyPaste';
@@ -943,7 +944,7 @@ export default mixins(
// newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60]; // newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60];
newNodeData.position = this.getNewNodePosition( newNodeData.position = this.getNewNodePosition(
[lastSelectedNode.position[0] + 150, lastSelectedNode.position[1]], [lastSelectedNode.position[0] + 150, lastSelectedNode.position[1]],
[100, 0] [100, 0],
); );
} else { } else {
// If no node is active find a free spot // If no node is active find a free spot
@@ -970,7 +971,7 @@ export default mixins(
// Add connections of active node to newly created one // Add connections of active node to newly created one
let connections = this.$store.getters.connectionsByNodeName( let connections = this.$store.getters.connectionsByNodeName(
lastSelectedNode.name lastSelectedNode.name,
); );
connections = JSON.parse(JSON.stringify(connections)); connections = JSON.parse(JSON.stringify(connections));
@@ -1406,7 +1407,7 @@ export default mixins(
newNodeData.position = this.getNewNodePosition( newNodeData.position = this.getNewNodePosition(
[node.position[0], node.position[1] + 150], [node.position[0], node.position[1] + 150],
[0, 150] [0, 150],
); );
await this.addNodes([newNodeData]); await this.addNodes([newNodeData]);
@@ -1504,7 +1505,7 @@ export default mixins(
nameInput.select(); nameInput.select();
} }
const promptResponse = await promptResponsePromise; const promptResponse = await promptResponsePromise as MessageBoxInputData;
this.renameNode(currentName, promptResponse.value); this.renameNode(currentName, promptResponse.value);
} catch (e) {} } catch (e) {}
@@ -1602,7 +1603,7 @@ export default mixins(
for (const type of Object.keys(connections[sourceNode])) { for (const type of Object.keys(connections[sourceNode])) {
for (let sourceIndex = 0; sourceIndex < connections[sourceNode][type].length; sourceIndex++) { for (let sourceIndex = 0; sourceIndex < connections[sourceNode][type].length; sourceIndex++) {
connections[sourceNode][type][sourceIndex].forEach(( connections[sourceNode][type][sourceIndex].forEach((
targetData targetData,
) => { ) => {
connectionData = [ connectionData = [
{ {

View File

@@ -23,7 +23,7 @@ module.exports = {
css: { css: {
loaderOptions: { loaderOptions: {
sass: { sass: {
data: ` prependData: `
@import "@/n8n-theme-variables.scss"; @import "@/n8n-theme-variables.scss";
`, `,
}, },

View File

@@ -58,11 +58,11 @@
"change-case": "^3.1.0", "change-case": "^3.1.0",
"copyfiles": "^2.1.1", "copyfiles": "^2.1.1",
"inquirer": "^7.0.0", "inquirer": "^7.0.0",
"n8n-core": "^0.10.0", "n8n-core": "^0.18.0",
"n8n-workflow": "^0.11.0", "n8n-workflow": "^0.18.0",
"replace-in-file": "^4.1.0", "replace-in-file": "^4.1.0",
"request": "^2.88.0", "request": "^2.88.0",
"tmp-promise": "^2.0.2", "tmp-promise": "^2.0.2",
"typescript": "~3.5.2" "typescript": "~3.7.4"
} }
} }

View File

@@ -63,9 +63,8 @@ export class Function implements INodeType {
console: 'inherit', console: 'inherit',
sandbox, sandbox,
require: { require: {
external: false, external: false as boolean | { modules: string[] },
builtin: [] as string[], builtin: [] as string[],
root: './',
} }
}; };
@@ -73,6 +72,11 @@ export class Function implements INodeType {
options.require.builtin = process.env.NODE_FUNCTION_ALLOW_BUILTIN.split(','); options.require.builtin = process.env.NODE_FUNCTION_ALLOW_BUILTIN.split(',');
} }
if (process.env.NODE_FUNCTION_ALLOW_EXTERNAL) {
options.require.external = { modules: process.env.NODE_FUNCTION_ALLOW_EXTERNAL.split(',') };
}
const vm = new NodeVM(options); const vm = new NodeVM(options);
// Get the code to execute // Get the code to execute
@@ -80,7 +84,7 @@ export class Function implements INodeType {
try { try {
// Execute the function code // Execute the function code
items = (await vm.run(`module.exports = async function() {${functionCode}}()`)); items = (await vm.run(`module.exports = async function() {${functionCode}}()`, './'));
} catch (e) { } catch (e) {
return Promise.reject(e); return Promise.reject(e);
} }

View File

@@ -93,7 +93,7 @@ export class GoogleSheet {
} }
); );
return response.data.values; return response.data.values as string[][] | undefined;
} }
@@ -141,9 +141,9 @@ export class GoogleSheet {
async batchUpdate(updateData: ISheetUpdateData[], valueInputMode: ValueInputOption) { async batchUpdate(updateData: ISheetUpdateData[], valueInputMode: ValueInputOption) {
const client = await this.getAuthenticationClient(); const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.values.batchUpdate( const response = await Sheets.spreadsheets.values.batchUpdate(
{ {
// @ts-ignore
auth: client, auth: client,
spreadsheetId: this.id, spreadsheetId: this.id,
valueInputOption: valueInputMode, valueInputOption: valueInputMode,
@@ -163,6 +163,7 @@ export class GoogleSheet {
async setData(range: string, data: string[][], valueInputMode: ValueInputOption) { async setData(range: string, data: string[][], valueInputMode: ValueInputOption) {
const client = await this.getAuthenticationClient(); const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.values.update( const response = await Sheets.spreadsheets.values.update(
{ {
// @ts-ignore // @ts-ignore
@@ -186,9 +187,9 @@ export class GoogleSheet {
async appendData(range: string, data: string[][], valueInputMode: ValueInputOption) { async appendData(range: string, data: string[][], valueInputMode: ValueInputOption) {
const client = await this.getAuthenticationClient(); const client = await this.getAuthenticationClient();
// @ts-ignore
const response = await Sheets.spreadsheets.values.append( const response = await Sheets.spreadsheets.values.append(
{ {
// @ts-ignore
auth: client, auth: client,
spreadsheetId: this.id, spreadsheetId: this.id,
range, range,

View File

@@ -0,0 +1,277 @@
import * as cheerio from 'cheerio';
import { IExecuteFunctions } from 'n8n-core';
import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
IDataObject,
} from 'n8n-workflow';
interface IValueData {
attribute?: string;
cssSelector: string;
returnValue: string;
key: string;
returnArray: boolean;
}
// The extraction functions
const extractFunctions: {
[key: string]: ($: Cheerio, valueData: IValueData) => string | undefined;
} = {
attribute: ($: Cheerio, valueData: IValueData): string | undefined => $.attr(valueData.attribute!),
html: ($: Cheerio, valueData: IValueData): string | undefined => $.html() || undefined,
text: ($: Cheerio, valueData: IValueData): string | undefined => $.text(),
value: ($: Cheerio, valueData: IValueData): string | undefined => $.val(),
};
/**
* Simple helper function which applies options
*/
function getValue($: Cheerio, valueData: IValueData, options: IDataObject) {
const value = extractFunctions[valueData.returnValue]($, valueData);
if (options.trimValues === false || value === undefined) {
return value;
}
return value.trim();
}
export class HtmlExtract implements INodeType {
description: INodeTypeDescription = {
displayName: 'HTML Extract',
name: 'htmlExtract',
icon: 'fa:cut',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["sourceData"] + ": " + $parameter["dataPropertyName"]}}',
description: 'Extracts data from HTML',
defaults: {
name: 'HTML Extract',
color: '#333377',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Source Data',
name: 'sourceData',
type: 'options',
options: [
{
name: 'Binary',
value: 'binary',
},
{
name: 'JSON',
value: 'json',
},
],
default: 'json',
description: 'If HTML should be read from binary or json data.',
},
{
displayName: 'Binary Property',
name: 'dataPropertyName',
type: 'string',
displayOptions: {
show: {
sourceData: [
'binary',
],
},
},
default: 'data',
required: true,
description: 'Name of the binary property in which the HTML to extract the data from can be found.',
},
{
displayName: 'JSON Property',
name: 'dataPropertyName',
type: 'string',
displayOptions: {
show: {
sourceData: [
'json',
],
},
},
default: 'data',
required: true,
description: 'Name of the json property in which the HTML to extract the data from can be found.<br />The property can either contain a string or an array of strings.',
},
{
displayName: 'Extraction Values',
name: 'extractionValues',
placeholder: 'Add Value',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'The extraction values.',
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The key under which the extracted value should be saved.',
},
{
displayName: 'CSS Selector',
name: 'cssSelector',
type: 'string',
default: '',
placeholder: '.price',
description: 'The CSS selector to use.',
},
{
displayName: 'Return Value',
name: 'returnValue',
type: 'options',
options: [
{
name: 'Attribute',
value: 'attribute',
description: 'Get an attribute value like "class" from an element.',
},
{
name: 'HTML',
value: 'html',
description: 'Get the HTML the element contains.',
},
{
name: 'Text',
value: 'text',
description: 'Get only the text content of the element.',
},
{
name: 'Value',
value: 'value',
description: 'Get value of an input, select or textarea.',
},
],
default: 'text',
description: 'What kind of data should be returned.',
},
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
displayOptions: {
show: {
returnValue: [
'attribute',
],
},
},
default: '',
placeholder: 'class',
description: 'The name of the attribute to return the value off.',
},
{
displayName: 'Return Array',
name: 'returnArray',
type: 'boolean',
default: false,
description: 'Returns the values as an array so if multiple ones get found they also get<br />returned separately.If not set all will be returned as a single string.',
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Trim Values',
name: 'trimValues',
type: 'boolean',
default: true,
description: 'Removes automatically all spaces and newlines from<br />the beginning and end of the values.',
},
],
}
]
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
let item: INodeExecutionData;
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex) as string;
const extractionValues = this.getNodeParameter('extractionValues', itemIndex) as IDataObject;
const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject;
const sourceData = this.getNodeParameter('sourceData', itemIndex) as string;
item = items[itemIndex];
let htmlArray: string[] | string = [];
if (sourceData === 'json') {
if (item.json[dataPropertyName] === undefined) {
throw new Error(`No property named "${dataPropertyName}" exists!`);
}
htmlArray = item.json[dataPropertyName] as string;
} else {
if (item.binary === undefined) {
throw new Error(`No item does not contain binary data!`);
}
if (item.binary[dataPropertyName] === undefined) {
throw new Error(`No property named "${dataPropertyName}" exists!`);
}
htmlArray = Buffer.from(item.binary[dataPropertyName].data, 'base64').toString('utf8');
}
// Convert it always to array that it works with a string or an array of strings
if (!Array.isArray(htmlArray)) {
htmlArray = [htmlArray];
}
for (const html of htmlArray as string[]) {
const $ = cheerio.load(html);
const newItem: INodeExecutionData = {
json: {},
};
// Itterate over all the defined values which should be extracted
let htmlElement;
for (const valueData of extractionValues.values as IValueData[]) {
htmlElement = $(valueData.cssSelector);
if (valueData.returnArray === true) {
// An array should be returned so itterate over one
// value at a time
newItem.json[valueData.key as string] = [];
htmlElement.each((i, el) => {
(newItem.json[valueData.key as string] as Array<string | undefined>).push(getValue($(el), valueData, options));
});
} else {
// One single value should be returned
newItem.json[valueData.key as string] = getValue(htmlElement, valueData, options);
}
}
returnData.push(newItem);
}
}
return this.prepareOutputData(returnData);
}
}

View File

@@ -25,6 +25,7 @@ export class HttpRequest implements INodeType {
icon: 'fa:at', icon: 'fa:at',
group: ['input'], group: ['input'],
version: 1, version: 1,
subtitle: '={{$parameter["requestMethod"] + ": " + $parameter["url"]}}',
description: 'Makes a HTTP request and returns the received data', description: 'Makes a HTTP request and returns the received data',
defaults: { defaults: {
name: 'HTTP Request', name: 'HTTP Request',

View File

@@ -109,27 +109,27 @@ export class SpreadsheetFile implements INodeType {
type: 'options', type: 'options',
options: [ options: [
{ {
name: 'csv', name: 'CSV',
value: 'csv', value: 'csv',
description: 'Comma-separated values', description: 'Comma-separated values',
}, },
{ {
name: 'ods', name: 'HTML',
value: 'ods',
description: 'OpenDocument Spreadsheet',
},
{
name: 'rtf',
value: 'rtf',
description: 'Rich Text Format',
},
{
name: 'html',
value: 'html', value: 'html',
description: 'HTML Table', description: 'HTML Table',
}, },
{ {
name: 'xls', name: 'ODS',
value: 'ods',
description: 'OpenDocument Spreadsheet',
},
{
name: 'RTF',
value: 'rtf',
description: 'Rich Text Format',
},
{
name: 'XLS',
value: 'xls', value: 'xls',
description: 'Excel', description: 'Excel',
}, },
@@ -166,22 +166,68 @@ export class SpreadsheetFile implements INodeType {
name: 'options', name: 'options',
type: 'collection', type: 'collection',
placeholder: 'Add Option', placeholder: 'Add Option',
displayOptions: {
show: {
operation: [
'toFile',
],
},
},
default: {}, default: {},
options: [ options: [
{ {
displayName: 'File Name', displayName: 'File Name',
name: 'fileName', name: 'fileName',
type: 'string', type: 'string',
displayOptions: {
show: {
'/operation': [
'toFile',
],
},
},
default: '', default: '',
description: 'File name to set in binary data. By default will "spreadsheet.<fileFormat>" be used.', description: 'File name to set in binary data. By default will "spreadsheet.<fileFormat>" be used.',
}, },
{
displayName: 'RAW Data',
name: 'rawData',
type: 'boolean',
displayOptions: {
show: {
'/operation': [
'fromFile'
],
},
},
default: false,
description: 'If the data should be returned RAW instead of parsed.',
},
{
displayName: 'Sheet Name',
name: 'sheetName',
type: 'string',
displayOptions: {
show: {
'/operation': [
'fromFile',
],
},
},
default: 'Sheet',
description: 'Name of the sheet to read from in the spreadsheet (if supported). If not set, the first one gets chosen.',
},
{
displayName: 'Sheet Name',
name: 'sheetName',
type: 'string',
displayOptions: {
show: {
'/operation': [
'toFile',
],
'/fileFormat': [
'ods',
'xls',
],
},
},
default: 'Sheet',
description: 'Name of the sheet to create in the spreadsheet.',
},
], ],
}, },
] ]
@@ -203,7 +249,8 @@ export class SpreadsheetFile implements INodeType {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
item = items[i]; item = items[i];
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string; const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
const options = this.getNodeParameter('options', i, {}) as IDataObject;
if (item.binary === undefined || item.binary[binaryPropertyName] === undefined) { if (item.binary === undefined || item.binary[binaryPropertyName] === undefined) {
// Property did not get found on item // Property did not get found on item
@@ -212,14 +259,22 @@ export class SpreadsheetFile implements INodeType {
// Read the binary spreadsheet data // Read the binary spreadsheet data
const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING); const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING);
const workbook = xlsxRead(binaryData); const workbook = xlsxRead(binaryData, { raw: options.rawData as boolean });
if (workbook.SheetNames.length === 0) { if (workbook.SheetNames.length === 0) {
throw new Error('File does not have any sheets!'); throw new Error('Spreadsheet does not have any sheets!');
}
let sheetName = workbook.SheetNames[0];
if (options.sheetName) {
if (!workbook.SheetNames.includes(options.sheetName as string)) {
throw new Error(`Spreadsheet does not contain sheet called "${options.sheetName}"!`);
}
sheetName = options.sheetName as string;
} }
// Convert it to json // Convert it to json
const sheetJson = xlsxUtils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]); const sheetJson = xlsxUtils.sheet_to_json(workbook.Sheets[sheetName]);
// Check if data could be found in file // Check if data could be found in file
if (sheetJson.length === 0) { if (sheetJson.length === 0) {
@@ -267,7 +322,7 @@ export class SpreadsheetFile implements INodeType {
} }
// Convert the data in the correct format // Convert the data in the correct format
const sheetName = 'Sheet'; const sheetName = options.sheetName as string || 'Sheet';
const wb: WorkBook = { const wb: WorkBook = {
SheetNames: [sheetName], SheetNames: [sheetName],
Sheets: { Sheets: {

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-nodes-base", "name": "n8n-nodes-base",
"version": "0.35.0", "version": "0.36.0",
"description": "Base nodes of n8n", "description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@@ -110,6 +110,7 @@
"dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleDrive.node.js",
"dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/Google/GoogleSheets.node.js",
"dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/GraphQL/GraphQL.node.js",
"dist/nodes/HtmlExtract/HtmlExtract.node.js",
"dist/nodes/HttpRequest.node.js", "dist/nodes/HttpRequest.node.js",
"dist/nodes/Hubspot/Hubspot.node.js", "dist/nodes/Hubspot/Hubspot.node.js",
"dist/nodes/If.node.js", "dist/nodes/If.node.js",
@@ -167,6 +168,7 @@
"devDependencies": { "devDependencies": {
"@types/aws4": "^1.5.1", "@types/aws4": "^1.5.1",
"@types/basic-auth": "^1.1.2", "@types/basic-auth": "^1.1.2",
"@types/cheerio": "^0.22.15",
"@types/cron": "^1.6.1", "@types/cron": "^1.6.1",
"@types/express": "^4.16.1", "@types/express": "^4.16.1",
"@types/gm": "^1.18.2", "@types/gm": "^1.18.2",
@@ -184,15 +186,16 @@
"n8n-workflow": "~0.18.0", "n8n-workflow": "~0.18.0",
"ts-jest": "^24.0.2", "ts-jest": "^24.0.2",
"tslint": "^5.17.0", "tslint": "^5.17.0",
"typescript": "~3.5.2" "typescript": "~3.7.4"
}, },
"dependencies": { "dependencies": {
"aws4": "^1.8.0", "aws4": "^1.8.0",
"basic-auth": "^2.0.1", "basic-auth": "^2.0.1",
"cheerio": "^1.0.0-rc.3",
"cron": "^1.6.0", "cron": "^1.6.0",
"glob-promise": "^3.4.0", "glob-promise": "^3.4.0",
"gm": "^1.23.1", "gm": "^1.23.1",
"googleapis": "^42.0.0", "googleapis": "^46.0.0",
"imap-simple": "^4.3.0", "imap-simple": "^4.3.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",

View File

@@ -27,12 +27,12 @@
"devDependencies": { "devDependencies": {
"@types/express": "^4.16.1", "@types/express": "^4.16.1",
"@types/jest": "^24.0.18", "@types/jest": "^24.0.18",
"@types/lodash.get": "^4.4.5", "@types/lodash.get": "^4.4.6",
"@types/node": "^10.10.1", "@types/node": "^10.10.1",
"jest": "^24.9.0", "jest": "^24.9.0",
"ts-jest": "^24.0.2", "ts-jest": "^24.0.2",
"tslint": "^5.17.0", "tslint": "^5.17.0",
"typescript": "~3.5.2" "typescript": "~3.7.4"
}, },
"dependencies": { "dependencies": {
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",