Initial commit to release
230
packages/nodes-base/LICENSE
Normal file
@@ -0,0 +1,230 @@
|
||||
“Commons Clause” License Condition v1.0
|
||||
|
||||
The Software is provided to you by the Licensor under the
|
||||
License, as defined below, subject to the following condition.
|
||||
|
||||
Without limiting other conditions in the License, the grant
|
||||
of rights under the License will not include, and the License
|
||||
does not grant to you, the right to Sell the Software.
|
||||
|
||||
For purposes of the foregoing, “Sell” means practicing any or
|
||||
all of the rights granted to you under the License to provide
|
||||
to third parties, for a fee or other consideration (including
|
||||
without limitation fees for hosting or consulting/ support
|
||||
services related to the Software), a product or service whose
|
||||
value derives, entirely or substantially, from the functionality
|
||||
of the Software. Any license notice or attribution required by
|
||||
the License must also include this Commons Clause License
|
||||
Condition notice.
|
||||
|
||||
Software: n8n
|
||||
|
||||
License: Apache 2.0
|
||||
|
||||
Licensor: Jan Oberhauser
|
||||
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
14
packages/nodes-base/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# n8n-nodes-base
|
||||
|
||||

|
||||
|
||||
The nodes which are included by default in n8n
|
||||
|
||||
```
|
||||
npm install n8n-nodes-base -g
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 with Commons Clause](LICENSE)
|
||||
18
packages/nodes-base/credentials/AsanaApi.credentials.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class AsanaApi implements ICredentialType {
|
||||
name = 'asanaApi';
|
||||
displayName = 'Asana API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
24
packages/nodes-base/credentials/ChargebeeApi.credentials.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class ChargebeeApi implements ICredentialType {
|
||||
name = 'chargebeeApi';
|
||||
displayName = 'Chargebee API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Account Name',
|
||||
name: 'accountName',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Api Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
18
packages/nodes-base/credentials/DropboxApi.credentials.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class DropboxApi implements ICredentialType {
|
||||
name = 'dropboxApi';
|
||||
displayName = 'Dropbox API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
24
packages/nodes-base/credentials/GithubApi.credentials.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class GithubApi implements ICredentialType {
|
||||
name = 'githubApi';
|
||||
displayName = 'Github API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
26
packages/nodes-base/credentials/GoogleApi.credentials.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class GoogleApi implements ICredentialType {
|
||||
name = 'googleApi';
|
||||
displayName = 'Google API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
|
||||
},
|
||||
{
|
||||
displayName: 'Private Key',
|
||||
name: 'privateKey',
|
||||
lines: 5,
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
28
packages/nodes-base/credentials/HttpBasicAuth.credentials.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class HttpBasicAuth implements ICredentialType {
|
||||
name = 'httpBasicAuth';
|
||||
displayName = 'Basic Auth';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
|
||||
},
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class HttpHeaderAuth implements ICredentialType {
|
||||
name = 'httpHeaderAuth';
|
||||
displayName = 'Header Auth';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
46
packages/nodes-base/credentials/Imap.credentials.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class Imap implements ICredentialType {
|
||||
name = 'imap';
|
||||
displayName = 'IMAP';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
|
||||
},
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Host',
|
||||
name: 'host',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Port',
|
||||
name: 'port',
|
||||
type: 'number' as NodePropertyTypes,
|
||||
default: 993,
|
||||
},
|
||||
{
|
||||
displayName: 'SSL/TLS',
|
||||
name: 'secure',
|
||||
type: 'boolean' as NodePropertyTypes,
|
||||
default: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
25
packages/nodes-base/credentials/LinkFishApi.credentials.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class LinkFishApi implements ICredentialType {
|
||||
name = 'linkFishApi';
|
||||
displayName = 'link.fish API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
|
||||
},
|
||||
{
|
||||
displayName: 'Api Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
42
packages/nodes-base/credentials/MailgunApi.credentials.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class MailgunApi implements ICredentialType {
|
||||
name = 'mailgunApi';
|
||||
displayName = 'Mailgun API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Domain',
|
||||
name: 'apiDomain',
|
||||
type: 'options' as NodePropertyTypes,
|
||||
options: [
|
||||
{
|
||||
name: 'api.eu.mailgun.net',
|
||||
value: 'api.eu.mailgun.net',
|
||||
},
|
||||
{
|
||||
name: 'api.mailgun.net',
|
||||
value: 'api.mailgun.net',
|
||||
},
|
||||
],
|
||||
default: 'api.mailgun.net',
|
||||
description: 'The configured mailgun API domain.',
|
||||
},
|
||||
{
|
||||
displayName: 'Email Domain',
|
||||
name: 'emailDomain',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
description: '.',
|
||||
},
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
32
packages/nodes-base/credentials/NextCloudApi.credentials.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class NextCloudApi implements ICredentialType {
|
||||
name = 'nextCloudApi';
|
||||
displayName = 'NextCloud API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Web DAV URL',
|
||||
name: 'webDavUrl',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
|
||||
},
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
|
||||
},
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class OpenWeatherMapApi implements ICredentialType {
|
||||
name = 'openWeatherMapApi';
|
||||
displayName = 'OpenWeatherMap API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
33
packages/nodes-base/credentials/Redis.credentials.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class Redis implements ICredentialType {
|
||||
name = 'redis';
|
||||
displayName = 'Redis';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Host',
|
||||
name: 'host',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: 'localhost',
|
||||
},
|
||||
{
|
||||
displayName: 'Port',
|
||||
name: 'port',
|
||||
type: 'number' as NodePropertyTypes,
|
||||
default: 6379,
|
||||
},
|
||||
];
|
||||
}
|
||||
18
packages/nodes-base/credentials/SlackApi.credentials.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class SlackApi implements ICredentialType {
|
||||
name = 'slackApi';
|
||||
displayName = 'Slack API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
46
packages/nodes-base/credentials/Smtp.credentials.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class Smtp implements ICredentialType {
|
||||
name = 'smtp';
|
||||
displayName = 'SMTP';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
|
||||
},
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Host',
|
||||
name: 'host',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Port',
|
||||
name: 'port',
|
||||
type: 'number' as NodePropertyTypes,
|
||||
default: 465,
|
||||
},
|
||||
{
|
||||
displayName: 'SSL/TLS',
|
||||
name: 'secure',
|
||||
type: 'boolean' as NodePropertyTypes,
|
||||
default: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
24
packages/nodes-base/credentials/TwilioApi.credentials.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class TwilioApi implements ICredentialType {
|
||||
name = 'twilioApi';
|
||||
displayName = 'Twilio API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Account SID',
|
||||
name: 'accountSid',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth Token',
|
||||
name: 'authToken',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
8
packages/nodes-base/gulpfile.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const { src, dest } = require('gulp');
|
||||
|
||||
function copyIcons() {
|
||||
return src('nodes/**/*.png')
|
||||
.pipe(dest('dist/nodes'));
|
||||
}
|
||||
|
||||
exports.default = copyIcons;
|
||||
422
packages/nodes-base/nodes/Asana/Asana.node.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodePropertyOptions,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
asanaApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class Asana implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Asana',
|
||||
name: 'asana',
|
||||
icon: 'file:asana.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Access and edit Asana tasks',
|
||||
defaults: {
|
||||
name: 'Asana',
|
||||
color: '#339922',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'asanaApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Create Task',
|
||||
value: 'createTask',
|
||||
description: 'Creates a task',
|
||||
},
|
||||
{
|
||||
name: 'Delete Task',
|
||||
value: 'deleteTask',
|
||||
description: 'Delete a task',
|
||||
},
|
||||
{
|
||||
name: 'Get Task',
|
||||
value: 'getTask',
|
||||
description: 'Get data of task',
|
||||
},
|
||||
{
|
||||
name: 'Update Task',
|
||||
value: 'updateTask',
|
||||
description: 'Update a task',
|
||||
},
|
||||
{
|
||||
name: 'Search For Tasks',
|
||||
value: 'searchForTasks',
|
||||
description: 'Search Tasks',
|
||||
},
|
||||
],
|
||||
default: 'createTask',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// createTask
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Workspace',
|
||||
name: 'workspace',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
options: [],
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'createTask',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The workspace to create the task in',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'taskName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'createTask',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The name of the task to create',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// deleteTask
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'deleteTask',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The ID of the task to delete.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// getTask
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getTask',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The ID of the task to get the data of.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// updateTask
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'updateTask',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The ID of the task to update the data of.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// searchForTasks
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Workspace',
|
||||
name: 'workspace',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
options: [],
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'searchForTasks',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The workspace to create the task in',
|
||||
},
|
||||
{
|
||||
displayName: 'Search Properties',
|
||||
name: 'searchTaskProperties',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'searchForTasks',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
description: 'Properties to search for',
|
||||
placeholder: 'Add Search Property',
|
||||
options: [
|
||||
// TODO: Add "assignee" and "assignee_status"
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
rows: 5,
|
||||
},
|
||||
default: '',
|
||||
description: 'Text to search for in name or notes.',
|
||||
},
|
||||
{
|
||||
displayName: 'Completed',
|
||||
name: 'completed',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'If the task is marked completed.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// createTask/updateTask
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Other Properties',
|
||||
name: 'otherProperties',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: [
|
||||
'deleteTask',
|
||||
'getTask',
|
||||
'searchForTasks',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
description: 'Other properties to set',
|
||||
placeholder: 'Add Property',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': [
|
||||
'updateTask',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The new name of the task',
|
||||
},
|
||||
// TODO: Add "assignee" and "assignee_status"
|
||||
{
|
||||
displayName: 'Notes',
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
rows: 5,
|
||||
},
|
||||
default: '',
|
||||
description: 'The task notes',
|
||||
},
|
||||
{
|
||||
displayName: 'Completed',
|
||||
name: 'completed',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'If the task should be marked completed.',
|
||||
},
|
||||
{
|
||||
displayName: 'Due On',
|
||||
name: 'due_on',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Date on which the time is due.',
|
||||
},
|
||||
{
|
||||
displayName: 'Liked',
|
||||
name: 'liked',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'If the task is liked by the authorized user.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the available workspaces to display them to user so that he can
|
||||
// select them easily
|
||||
async getWorkspaces(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const endpoint = 'workspaces';
|
||||
const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {});
|
||||
|
||||
if (responseData.data === undefined) {
|
||||
throw new Error('No data got returned');
|
||||
}
|
||||
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
for (const workspaceData of responseData.data) {
|
||||
if (workspaceData.resource_type !== 'workspace') {
|
||||
// Not sure if for some reason also ever other resources
|
||||
// get returned but just in case filter them out
|
||||
continue;
|
||||
}
|
||||
|
||||
returnData.push({
|
||||
name: workspaceData.name,
|
||||
value: workspaceData.gid,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const credentials = this.getCredentials('asanaApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
let endpoint = '';
|
||||
let requestMethod = '';
|
||||
|
||||
let body: IDataObject;
|
||||
let qs: IDataObject;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
body = {};
|
||||
qs = {};
|
||||
|
||||
if (operation === 'createTask') {
|
||||
// ----------------------------------
|
||||
// createTask
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
endpoint = 'tasks';
|
||||
// endpoint = this.getNodeParameter('folderCreate', i) as string;
|
||||
|
||||
body.name = this.getNodeParameter('taskName', 0) as string;
|
||||
// body.notes = this.getNodeParameter('taskNotes', 0) as string;
|
||||
body.workspace = this.getNodeParameter('workspace', 0) as string;
|
||||
|
||||
const otherProperties = this.getNodeParameter('otherProperties', i) as IDataObject;
|
||||
Object.assign(body, otherProperties);
|
||||
|
||||
} else if (operation === 'deleteTask') {
|
||||
// ----------------------------------
|
||||
// deleteTask
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'DELETE';
|
||||
endpoint = 'tasks/' + this.getNodeParameter('taskId', i) as string;
|
||||
|
||||
} else if (operation === 'getTask') {
|
||||
// ----------------------------------
|
||||
// getTask
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'GET';
|
||||
endpoint = 'tasks/' + this.getNodeParameter('taskId', i) as string;
|
||||
|
||||
} else if (operation === 'updateTask') {
|
||||
// ----------------------------------
|
||||
// getTask
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'PUT';
|
||||
endpoint = 'tasks/' + this.getNodeParameter('taskId', i) as string;
|
||||
|
||||
|
||||
|
||||
const otherProperties = this.getNodeParameter('otherProperties', i) as IDataObject;
|
||||
Object.assign(body, otherProperties);
|
||||
|
||||
} else if (operation === 'searchForTasks') {
|
||||
// ----------------------------------
|
||||
// searchForTasks
|
||||
// ----------------------------------
|
||||
|
||||
const workspaceId = this.getNodeParameter('workspace', i) as string;
|
||||
|
||||
requestMethod = 'GET';
|
||||
endpoint = `workspaces/${workspaceId}/tasks/search`;
|
||||
|
||||
const searchTaskProperties = this.getNodeParameter('searchTaskProperties', i) as IDataObject;
|
||||
Object.assign(qs, searchTaskProperties);
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not known!`);
|
||||
}
|
||||
|
||||
const responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body);
|
||||
|
||||
returnData.push(responseData.data as IDataObject);
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
184
packages/nodes-base/nodes/Asana/AsanaTrigger.node.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeType,
|
||||
IWebhookResonseData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
asanaApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import { createHmac } from 'crypto';
|
||||
|
||||
export class AsanaTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Asana Trigger',
|
||||
name: 'asanaTrigger',
|
||||
icon: 'file:asana.png',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when Asana events occure.',
|
||||
defaults: {
|
||||
name: 'Asana Trigger',
|
||||
color: '#559922',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'asanaApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
reponseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'The resource ID to subscribe to. The resource can be a task or project.',
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
// @ts-ignore (because of request)
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
if (webhookData.webhookId === undefined) {
|
||||
// No webhook id is set so no webhook can exist
|
||||
return false;
|
||||
}
|
||||
|
||||
// Webhook got created before so check if it still exists
|
||||
const endpoint = `webhooks/${webhookData.webhookId}`;
|
||||
|
||||
try {
|
||||
await asanaApiRequest.call(this, 'GET', endpoint, {});
|
||||
} catch (e) {
|
||||
if (e.statusCode === 404) {
|
||||
// Webhook does not exist
|
||||
delete webhookData.webhookId;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some error occured
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If it did not error then the webhook exists
|
||||
return true;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
|
||||
const resource = this.getNodeParameter('resource') as string;
|
||||
|
||||
const endpont = `webhooks`;
|
||||
|
||||
const body = {
|
||||
resource,
|
||||
target: webhookUrl,
|
||||
};
|
||||
|
||||
const responseData = await asanaApiRequest.call(this, 'POST', endpont, body);
|
||||
|
||||
if (responseData.data === undefined || responseData.data.id === undefined) {
|
||||
// Required data is missing so was not successful
|
||||
return false;
|
||||
}
|
||||
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
webhookData.webhookId = responseData.data.id as string;
|
||||
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
if (webhookData.webhookId !== undefined) {
|
||||
const endpoint = `webhooks/${webhookData.webhookId}`;
|
||||
const body = {};
|
||||
|
||||
try {
|
||||
await asanaApiRequest.call(this, 'DELETE', endpoint, body);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove from the static workflow data so that it is clear
|
||||
// that no webhooks are registred anymore
|
||||
delete webhookData.webhookId;
|
||||
delete webhookData.webhookEvents;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResonseData> {
|
||||
const bodyData = this.getBodyData() as IDataObject;
|
||||
const headerData = this.getHeaderData() as IDataObject;
|
||||
const req = this.getRequestObject();
|
||||
|
||||
const webhookData = this.getWorkflowStaticData('node') as IDataObject;
|
||||
|
||||
|
||||
if (headerData['x-hook-secret'] !== undefined) {
|
||||
// Is a create webhook confirmation request
|
||||
webhookData.hookSecret = headerData['x-hook-secret'];
|
||||
|
||||
const res = this.getResponseObject();
|
||||
res.set('X-Hook-Secret', webhookData.hookSecret as string);
|
||||
res.status(200).end();
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Is regular webhook call
|
||||
// Check if it contains any events
|
||||
if (bodyData.events === undefined || !Array.isArray(bodyData.events) ||
|
||||
bodyData.events.length === 0) {
|
||||
// Does not contain any event data so nothing to process so no reason to
|
||||
// start the workflow
|
||||
return {};
|
||||
}
|
||||
|
||||
// Check if the request is valid
|
||||
// (if the signature matches to data and hookSecret)
|
||||
const computedSignature = createHmac("sha256", webhookData.hookSecret as string).update(JSON.stringify(req.body)).digest("hex");
|
||||
if (headerData['x-hook-signature'] !== computedSignature) {
|
||||
// Signature is not valid so ignore call
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
workflowData: [
|
||||
this.helpers.returnJsonArray(req.body)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
56
packages/nodes-base/nodes/Asana/GenericFunctions.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
|
||||
/**
|
||||
* Make an API request to Asana
|
||||
*
|
||||
* @param {IHookFunctions} this
|
||||
* @param {string} method
|
||||
* @param {string} url
|
||||
* @param {object} body
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('asanaApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${credentials.accessToken}`,
|
||||
},
|
||||
method,
|
||||
body: { data: body },
|
||||
qs: query,
|
||||
uri: `https://app.asana.com/api/1.0/${endpoint}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 401) {
|
||||
// Return a clear error
|
||||
throw new Error('The Asana credentials are not valid!');
|
||||
}
|
||||
|
||||
if (error.response && error.response.body && error.response.body.errors) {
|
||||
// Try to return the error prettier
|
||||
const errorMessages = error.response.body.errors.map((errorData: { message: string }) => {
|
||||
return errorData.message;
|
||||
});
|
||||
throw new Error(`Asana error response [${error.statusCode}]: ${errorMessages.join(' | ')}`);
|
||||
}
|
||||
|
||||
// If that data does not exist for some reason return the actual error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Asana/asana.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
507
packages/nodes-base/nodes/Chargebee/Chargebee.node.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as requestPromise from 'request-promise-native';
|
||||
|
||||
interface CustomProperty {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface FilterValue {
|
||||
operation: string;
|
||||
value: NodeParameterValue;
|
||||
}
|
||||
interface FilterValues {
|
||||
[key: string]: FilterValue[];
|
||||
}
|
||||
|
||||
|
||||
export class Chargebee implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Chargebee',
|
||||
name: 'chargebee',
|
||||
icon: 'file:chargebee.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Retrieve data from Chargebee API',
|
||||
defaults: {
|
||||
name: 'Chargebee',
|
||||
color: '#22BB11',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'chargebeeApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
default: 'listInvoices',
|
||||
description: 'The operation to perform.',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Cancel Subscription',
|
||||
value: 'cancelSubscription',
|
||||
description: 'Cancel a subscription',
|
||||
},
|
||||
{
|
||||
name: 'Create Customer',
|
||||
value: 'createCustomer',
|
||||
description: 'Create a customer',
|
||||
},
|
||||
{
|
||||
name: 'Delete Subscription',
|
||||
value: 'deleteSubscription',
|
||||
description: 'Deletes a subscription',
|
||||
},
|
||||
{
|
||||
name: 'List Invoices',
|
||||
value: 'listInvoices',
|
||||
description: 'Returns the invoices',
|
||||
},
|
||||
{
|
||||
name: 'PDF Invoice URL',
|
||||
value: 'pdfInvoiceUrl',
|
||||
description: 'Gets PDF invoice URL and adds it as property "pdfUrl"',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// cancelSubscription
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Subscription Id',
|
||||
name: 'subscriptionId',
|
||||
description: 'The id of the subscription to cancel.',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'cancelSubscription'
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Schedule end of Term',
|
||||
name: 'endOfTerm',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'cancelSubscription'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'If set it will not cancel it directly in will instead schedule the cancelation for the end of the term..',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// createCustomer
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Properties',
|
||||
name: 'properties',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'createCustomer'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
description: 'Properties to set on the new user',
|
||||
placeholder: 'Add Property',
|
||||
options: [
|
||||
{
|
||||
displayName: 'User Id',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Id for the new customer. If not given, this will be auto-generated.',
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'first_name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The first name of the customer.',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'last_name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The last name of the customer.',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The email address of the customer.',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The phone number of the customer.',
|
||||
},
|
||||
{
|
||||
displayName: 'Company',
|
||||
name: 'company',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The company of the customer.',
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
displayName: 'Custom Properties',
|
||||
name: 'customProperties',
|
||||
placeholder: 'Add Custom Property',
|
||||
description: 'Adds a custom property to set also values which have not been predefined.',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'property',
|
||||
displayName: 'Property',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the property to set.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value of the property to set.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// deleteSubscription
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Subscription Id',
|
||||
name: 'subscriptionId',
|
||||
description: 'The id of the subscription to delete.',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'deleteSubscription'
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// pdfInvoiceUrl
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Invoice Id',
|
||||
name: 'invoiceId',
|
||||
description: 'The id of the invoice to get.',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'pdfInvoiceUrl'
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// listInvoices
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Max results',
|
||||
name: 'maxResults',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 10,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'listInvoices'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Max. amount of results to return(< 100).',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
placeholder: 'Add Filter',
|
||||
description: 'Filter for invoices.',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'listInvoices'
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'date',
|
||||
displayName: 'Invoice Date',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Is',
|
||||
value: 'is'
|
||||
},
|
||||
{
|
||||
name: 'Is Not',
|
||||
value: 'is_not'
|
||||
},
|
||||
{
|
||||
name: 'After',
|
||||
value: 'after'
|
||||
},
|
||||
{
|
||||
name: 'Before',
|
||||
value: 'before'
|
||||
},
|
||||
|
||||
],
|
||||
default: 'after',
|
||||
description: 'Operation to decide where the the data should be mapped to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Date',
|
||||
name: 'value',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Query date.',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
displayName: 'Invoice Amount',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Is',
|
||||
value: 'is'
|
||||
},
|
||||
{
|
||||
name: 'Is Not',
|
||||
value: 'is_not'
|
||||
},
|
||||
{
|
||||
name: 'Greater than',
|
||||
value: 'gt'
|
||||
},
|
||||
{
|
||||
name: 'Greater equal than',
|
||||
value: 'gte'
|
||||
},
|
||||
{
|
||||
name: 'Less than',
|
||||
value: 'lt'
|
||||
},
|
||||
{
|
||||
name: 'Less equal than',
|
||||
value: 'lte'
|
||||
},
|
||||
],
|
||||
default: 'gt',
|
||||
description: 'Operation to decide where the the data should be mapped to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Amount',
|
||||
name: 'value',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
numberPrecision: 2,
|
||||
},
|
||||
default: 0,
|
||||
description: 'Query amount.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
let item: INodeExecutionData;
|
||||
|
||||
const credentials = this.getCredentials('chargebeeApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const baseUrl = `https://${credentials.accountName}.chargebee.com/api/v2`;
|
||||
|
||||
// For Post
|
||||
let body: IDataObject;
|
||||
// For Query string
|
||||
let qs: IDataObject;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
item = items[i];
|
||||
const operation = this.getNodeParameter('operation', i) as string;
|
||||
|
||||
let requestMethod = 'GET';
|
||||
let endpoint = '';
|
||||
body = {};
|
||||
qs = {};
|
||||
|
||||
if (operation === 'cancelSubscription') {
|
||||
requestMethod = 'POST';
|
||||
|
||||
const subscriptionId = this.getNodeParameter('subscriptionId', i, {}) as string;
|
||||
body.end_of_term = this.getNodeParameter('endOfTerm', i, {}) as boolean;
|
||||
|
||||
endpoint = `subscriptions/${subscriptionId.trim()}/cancel`;
|
||||
} else if (operation === 'createCustomer') {
|
||||
requestMethod = 'POST';
|
||||
|
||||
const properties = this.getNodeParameter('properties', i, {}) as IDataObject;
|
||||
|
||||
for (const key of Object.keys(properties)) {
|
||||
if (key === 'customProperties' && (properties.customProperties as IDataObject).property !== undefined) {
|
||||
for (const customProperty of (properties.customProperties as IDataObject)!.property! as CustomProperty[]) {
|
||||
qs[customProperty.name] = customProperty.value;
|
||||
}
|
||||
} else {
|
||||
qs[key] = properties[key];
|
||||
}
|
||||
}
|
||||
|
||||
endpoint = `customers`;
|
||||
} else if (operation === 'deleteSubscription') {
|
||||
requestMethod = 'POST';
|
||||
|
||||
const subscriptionId = this.getNodeParameter('subscriptionId', i, {}) as string;
|
||||
|
||||
endpoint = `subscriptions/${subscriptionId.trim()}/delete`;
|
||||
} else if (operation === 'listInvoices') {
|
||||
endpoint = 'invoices';
|
||||
// TODO: Make also sorting configurable
|
||||
qs['sort_by[desc]'] = 'date';
|
||||
|
||||
qs.limit = this.getNodeParameter('maxResults', i, {});
|
||||
|
||||
const setFilters: FilterValues = this.getNodeParameter('filters', i, {}) as unknown as FilterValues;
|
||||
|
||||
let filter: FilterValue;
|
||||
let value: NodeParameterValue;
|
||||
|
||||
for (const filterProperty of Object.keys(setFilters)) {
|
||||
for (filter of setFilters[filterProperty]) {
|
||||
value = filter.value;
|
||||
if (filterProperty === 'date') {
|
||||
value = Math.floor(new Date(value as string).getTime() / 1000);
|
||||
}
|
||||
qs[`${filterProperty}[${filter.operation}]`] = value;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (operation === 'pdfInvoiceUrl') {
|
||||
requestMethod = 'POST';
|
||||
const invoiceId = this.getNodeParameter('invoiceId', i) as string;
|
||||
endpoint = `invoices/${invoiceId.trim()}/pdf`;
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: requestMethod,
|
||||
body,
|
||||
qs,
|
||||
uri: `${baseUrl}/${endpoint}`,
|
||||
auth: {
|
||||
user: credentials.apiKey as string,
|
||||
pass: '',
|
||||
},
|
||||
json: true
|
||||
};
|
||||
|
||||
const responseData = await requestPromise(options);
|
||||
|
||||
if (operation === 'listInvoices') {
|
||||
responseData.list.forEach((data: IDataObject) => {
|
||||
returnData.push(data.invoice as IDataObject);
|
||||
});
|
||||
} else if (operation === 'pdfInvoiceUrl') {
|
||||
item.json.pdfUrl = responseData.download.download_url;
|
||||
returnData.push(item.json);
|
||||
} else {
|
||||
returnData.push(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
245
packages/nodes-base/nodes/Chargebee/ChargebeeTrigger.node.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import {
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeType,
|
||||
IWebhookResonseData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class ChargebeeTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Chargebee Trigger',
|
||||
name: 'chargebeeTrigger',
|
||||
icon: 'file:chargebee.png',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when Chargebee events occure.',
|
||||
defaults: {
|
||||
name: 'Chargebee Trigger',
|
||||
color: '#559922',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
reponseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
type: 'multiOptions',
|
||||
required: true,
|
||||
default: [],
|
||||
description: 'The operation to perform.',
|
||||
options: [
|
||||
{
|
||||
name: '*',
|
||||
value: '*',
|
||||
description: 'Any time any event is triggered (Wildcard Event).',
|
||||
},
|
||||
{
|
||||
name: 'Customer Created',
|
||||
value: 'customer_created',
|
||||
description: 'Triggered when a customer is created.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Changed',
|
||||
value: 'customer_changed',
|
||||
description: 'Triggered when a customer is changed.',
|
||||
},
|
||||
{
|
||||
name: 'Customer Deleted',
|
||||
value: 'customer_deleted',
|
||||
description: 'Triggered when a customer is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Created',
|
||||
value: 'subscription_created',
|
||||
description: 'Triggered when a new subscription is created.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Started',
|
||||
value: 'subscription_started',
|
||||
description: 'Triggered when a "future" subscription gets started.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Trial Ending',
|
||||
value: 'subscription_trial_ending',
|
||||
description: 'Triggered 6 days prior to the trial period\'s end date.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Activated',
|
||||
value: 'subscription_activated',
|
||||
description: 'Triggered after the subscription has been moved from "Trial" to "Active" state.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Changed',
|
||||
value: 'subscription_changed',
|
||||
description: 'Triggered when the subscription\'s recurring items are changed.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Cancellation Scheduled',
|
||||
value: 'subscription_cancellation_scheduled',
|
||||
description: 'Triggered when subscription is scheduled to cancel at end of current term.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Cancelling',
|
||||
value: 'subscription_cancelling',
|
||||
description: 'Triggered 6 days prior to the scheduled cancellation date.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Cancelled',
|
||||
value: 'subscription_cancelled',
|
||||
description: 'Triggered when the subscription is cancelled. If it is cancelled due to non payment or because the card details are not present, the subscription will have the possible reason as "cancel_reason".',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Reactivated',
|
||||
value: 'subscription_reactivated',
|
||||
description: 'Triggered when the subscription is moved from cancelled state to "Active" or "Trial" state.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Renewed',
|
||||
value: 'subscription_renewed',
|
||||
description: 'Triggered when the subscription is renewed from the current term.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Scheduled Cancellation Removed',
|
||||
value: 'subscription_scheduled_cancellation_removed',
|
||||
description: 'Triggered when scheduled cancellation is removed for the subscription.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Shipping Address Updated',
|
||||
value: 'subscription_shipping_address_updated',
|
||||
description: 'Triggered when shipping address is added or updated for a subscription.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Deleted',
|
||||
value: 'subscription_deleted',
|
||||
description: 'Triggered when a subscription is deleted. ',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Created',
|
||||
value: 'invoice_created',
|
||||
description: 'Event triggered (in the case of metered billing) when a "Pending" invoice is created that has usage related charges or line items to be added, before being closed. This is triggered only when the “Notify for Pending Invoices” option is enabled.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Generated',
|
||||
value: 'invoice_generated',
|
||||
description: 'Event triggered when a new invoice is generated. In case of metered billing, this event is triggered when a "Pending" invoice is closed.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Updated',
|
||||
value: 'invoice_updated',
|
||||
description: 'Triggered when the invoice’s shipping/billing address is updated, if the invoice is voided, or when the amount due is modified due to payments applied/removed.',
|
||||
},
|
||||
{
|
||||
name: 'Invoice Deleted',
|
||||
value: 'invoice_deleted',
|
||||
description: 'Event triggered when an invoice is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Renewal Reminder',
|
||||
value: 'subscription_renewal_reminder',
|
||||
description: 'Triggered 3 days before each subscription\'s renewal.',
|
||||
},
|
||||
{
|
||||
name: 'Transaction Created',
|
||||
value: 'transaction_created',
|
||||
description: 'Triggered when a transaction is recorded.',
|
||||
},
|
||||
{
|
||||
name: 'Transaction Updated',
|
||||
value: 'transaction_updated',
|
||||
description: 'Triggered when a transaction is updated. E.g. (1) When a transaction is removed, (2) or when an excess payment is applied on an invoice, (3) or when amount_capturable gets updated.',
|
||||
},
|
||||
{
|
||||
name: 'Transaction Deleted',
|
||||
value: 'transaction_deleted',
|
||||
description: 'Triggered when a transaction is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Succeeded',
|
||||
value: 'payment_succeeded',
|
||||
description: 'Triggered when the payment is successfully collected.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Failed',
|
||||
value: 'payment_failed',
|
||||
description: 'Triggered when attempt to charge customer\'s credit card fails.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Refunded',
|
||||
value: 'payment_refunded',
|
||||
description: 'Triggered when a payment refund is made.',
|
||||
},
|
||||
{
|
||||
name: 'Payment Initiated',
|
||||
value: 'payment_initiated',
|
||||
description: 'Triggered when a payment is initiated via direct debit.',
|
||||
},
|
||||
{
|
||||
name: 'Refund Initiated',
|
||||
value: 'refund_initiated',
|
||||
description: 'Triggered when a refund is initiated via direct debit.',
|
||||
},
|
||||
{
|
||||
name: 'Card Added',
|
||||
value: 'card_added',
|
||||
description: 'Triggered when a card is added for a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Card Updated',
|
||||
value: 'card_updated',
|
||||
description: 'Triggered when the card is updated for a customer.',
|
||||
},
|
||||
{
|
||||
name: 'Card Expiring',
|
||||
value: 'card_expiring',
|
||||
description: 'Triggered when the customer\'s credit card is expiring soon.Triggered 30 days before the expiry date.',
|
||||
},
|
||||
{
|
||||
name: 'Card Expired',
|
||||
value: 'card_expired',
|
||||
description: 'Triggered when the card for a customer has expired.',
|
||||
},
|
||||
{
|
||||
name: 'Card Deleted',
|
||||
value: 'card_deleted',
|
||||
description: 'Triggered when a card is deleted for a customer.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResonseData> {
|
||||
const bodyData = this.getBodyData() as IDataObject;
|
||||
const req = this.getRequestObject();
|
||||
|
||||
const events = this.getNodeParameter('events', []) as string[];
|
||||
|
||||
const eventType = bodyData.event_type as string | undefined;
|
||||
|
||||
if (eventType === undefined || !events.includes('*') && !events.includes(eventType)) {
|
||||
// If not eventType is defined or when one is defined but we are not
|
||||
// listening to it do not start the workflow.
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
workflowData: [
|
||||
this.helpers.returnJsonArray(req.body)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Chargebee/chargebee.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
236
packages/nodes-base/nodes/Cron.node.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { ITriggerFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ITriggerResponse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { CronJob } from 'cron';
|
||||
|
||||
interface TriggerTime {
|
||||
mode: string;
|
||||
hour: number;
|
||||
minute: number;
|
||||
dayOfMonth: number;
|
||||
weekeday: number;
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
|
||||
export class Cron implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Cron',
|
||||
name: 'cron',
|
||||
icon: 'fa:calendar',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Triggers the workflow at a specific time',
|
||||
defaults: {
|
||||
name: 'Cron',
|
||||
color: '#00FF00',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Trigger Times',
|
||||
name: 'triggerTimes',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
multipleValueButtonText: 'Add Time',
|
||||
},
|
||||
default: {},
|
||||
description: 'Triggers for the workflow',
|
||||
placeholder: 'Add Cron Time',
|
||||
options: [
|
||||
{
|
||||
name: 'item',
|
||||
displayName: 'Item',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Mode',
|
||||
name: 'mode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Every Day',
|
||||
value: 'everyDay'
|
||||
},
|
||||
{
|
||||
name: 'Every Week',
|
||||
value: 'everyWeek'
|
||||
},
|
||||
{
|
||||
name: 'Every Month',
|
||||
value: 'everyMonth'
|
||||
},
|
||||
],
|
||||
default: 'everyDay',
|
||||
description: 'How often to trigger.',
|
||||
},
|
||||
{
|
||||
displayName: 'Hour',
|
||||
name: 'hour',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 23,
|
||||
},
|
||||
default: 14,
|
||||
description: 'The hour of the day to trigger (24h format).',
|
||||
},
|
||||
{
|
||||
displayName: 'Minute',
|
||||
name: 'minute',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 59,
|
||||
},
|
||||
default: 0,
|
||||
description: 'The minute of the day to trigger.',
|
||||
},
|
||||
{
|
||||
displayName: 'Day of Month',
|
||||
name: 'dayOfMonth',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: [
|
||||
'everyMonth',
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 31,
|
||||
},
|
||||
default: 1,
|
||||
description: 'The day of the month to trigger.',
|
||||
},
|
||||
{
|
||||
displayName: 'Weekday',
|
||||
name: 'weekday',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: [
|
||||
'everyWeek',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Monday',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
name: 'Tuesday',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
name: 'Wednesday',
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
name: 'Thursday',
|
||||
value: '4',
|
||||
},
|
||||
{
|
||||
name: 'Friday',
|
||||
value: '5',
|
||||
},
|
||||
{
|
||||
name: 'Saturday',
|
||||
value: '6',
|
||||
},
|
||||
{
|
||||
name: 'Sunday',
|
||||
value: '0',
|
||||
},
|
||||
],
|
||||
default: '1',
|
||||
description: 'The weekday to trigger.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
|
||||
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||
|
||||
const triggerTimes = this.getNodeParameter('triggerTimes') as unknown as {
|
||||
item: TriggerTime[];
|
||||
};
|
||||
|
||||
// Define the order the cron-time-parameter appear
|
||||
const parameterOrder = [
|
||||
'second', // 0 - 59
|
||||
'minute', // 0 - 59
|
||||
'hour', // 0 - 23
|
||||
'dayOfMonth', // 1 - 31
|
||||
'month', // 0 - 11(Jan - Dec)
|
||||
'weekday', // 0 - 6(Sun - Sat)
|
||||
];
|
||||
|
||||
// Get all the trigger times
|
||||
const cronTimes: string[] = [];
|
||||
let cronTime: string[];
|
||||
let parameterName: string;
|
||||
if (triggerTimes.item !== undefined) {
|
||||
for (const item of triggerTimes.item) {
|
||||
cronTime = [];
|
||||
for (parameterName of parameterOrder) {
|
||||
if (item[parameterName] !== undefined) {
|
||||
// Value is set so use it
|
||||
cronTime.push(item[parameterName] as string);
|
||||
} else if (parameterName === 'second') {
|
||||
// For seconds we use by default a random one to make sure to
|
||||
// balance the load a little bit over time
|
||||
cronTime.push(Math.floor(Math.random() * 60).toString());
|
||||
} else {
|
||||
// For all others set "any"
|
||||
cronTime.push('*');
|
||||
}
|
||||
}
|
||||
|
||||
cronTimes.push(cronTime.join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
// The trigger function to execute when the cron-time got reached
|
||||
// or when manually triggered
|
||||
const executeTrigger = () => {
|
||||
this.emit([this.helpers.returnJsonArray([{}])]);
|
||||
};
|
||||
|
||||
const timezone = this.getTimezone();
|
||||
|
||||
// Start the cron-jobs
|
||||
const cronJobs: CronJob[] = [];
|
||||
for (const cronTime of cronTimes) {
|
||||
cronJobs.push(new CronJob(cronTime, executeTrigger, undefined, true, timezone));
|
||||
}
|
||||
|
||||
// Stop the cron-jobs
|
||||
async function closeFunction() {
|
||||
for (const cronJob of cronJobs) {
|
||||
cronJob.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function manualTriggerFunction() {
|
||||
executeTrigger();
|
||||
}
|
||||
|
||||
return {
|
||||
closeFunction,
|
||||
manualTriggerFunction,
|
||||
};
|
||||
}
|
||||
}
|
||||
542
packages/nodes-base/nodes/Dropbox/Dropbox.node.ts
Normal file
@@ -0,0 +1,542 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
|
||||
export class Dropbox implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Dropbox',
|
||||
name: 'dropbox',
|
||||
icon: 'file:dropbox.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Access data on Dropbox',
|
||||
defaults: {
|
||||
name: 'Dropbox',
|
||||
color: '#22BB44',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'dropboxApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Copy',
|
||||
value: 'copy',
|
||||
description: 'Copy a file or folder',
|
||||
},
|
||||
{
|
||||
name: 'Create Folder',
|
||||
value: 'createFolder',
|
||||
description: 'Creates a folder',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Deletes a file or folder',
|
||||
},
|
||||
{
|
||||
name: 'Download File',
|
||||
value: 'downloadFile',
|
||||
description: 'Downloads a file',
|
||||
},
|
||||
{
|
||||
name: 'Get Folder Content',
|
||||
value: 'listFolderContent',
|
||||
description: 'Returns the files and folder in a given folder',
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
value: 'move',
|
||||
description: 'Moves a file or folder',
|
||||
},
|
||||
{
|
||||
name: 'Upload File',
|
||||
value: 'uploadFile',
|
||||
description: 'Uploads a file into a folder',
|
||||
},
|
||||
],
|
||||
default: 'uploadFile',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// copy
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'From Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'copy'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/original.txt',
|
||||
description: 'The path of file or folder to copy.',
|
||||
},
|
||||
{
|
||||
displayName: 'To Path',
|
||||
name: 'toPath',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'copy'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/copy.txt',
|
||||
description: 'The destination path of file or folder.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// createFolder
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Folder',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'createFolder'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/2019',
|
||||
description: 'The folder to create. The parent folder has to exist.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Delete Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'delete'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/2019/invoice_1.pdf',
|
||||
description: 'The path to delete. Can be a single file or a whole folder.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// downloadFile
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'File Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'downloadFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/2019/invoice_1.pdf',
|
||||
description: 'The file path of the file to download. Has to contain the full path.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'downloadFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Name of the binary property to which to<br />write the data of the read file.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// listFolderContent
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Folder Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'listFolderContent'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/2019/',
|
||||
description: 'The path of which to list the content.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// move
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'From Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'move'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/old_name.txt',
|
||||
description: 'The path of file or folder to move.',
|
||||
},
|
||||
{
|
||||
displayName: 'To Path',
|
||||
name: 'toPath',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'move'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/new_name.txt',
|
||||
description: 'The new path of file or folder.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// uploadFile
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'File Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'uploadFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/2019/invoice_1.pdf',
|
||||
description: 'The file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Data',
|
||||
name: 'binaryData',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'uploadFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'If the data to upload should be taken from binary field.',
|
||||
},
|
||||
{
|
||||
displayName: 'File Content',
|
||||
name: 'fileContent',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'uploadFile'
|
||||
],
|
||||
binaryData: [
|
||||
false
|
||||
],
|
||||
},
|
||||
|
||||
},
|
||||
placeholder: '',
|
||||
description: 'The text content of the file to upload.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'uploadFile'
|
||||
],
|
||||
binaryData: [
|
||||
true
|
||||
],
|
||||
},
|
||||
|
||||
},
|
||||
placeholder: '',
|
||||
description: 'Name of the binary property which contains<br />the data for the file to be uploaded.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const credentials = this.getCredentials('dropboxApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
let endpoint = '';
|
||||
let requestMethod = '';
|
||||
let body: IDataObject | Buffer;
|
||||
let isJson = false;
|
||||
|
||||
let headers: IDataObject;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
body = {};
|
||||
headers = {
|
||||
'Authorization': `Bearer ${credentials.accessToken}`,
|
||||
};
|
||||
|
||||
if (operation === 'copy') {
|
||||
// ----------------------------------
|
||||
// copy
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
isJson = true;
|
||||
body = {
|
||||
from_path: this.getNodeParameter('path', i) as string,
|
||||
to_path: this.getNodeParameter('toPath', i) as string,
|
||||
};
|
||||
|
||||
endpoint = 'https://api.dropboxapi.com/2/files/copy_v2';
|
||||
|
||||
} else if (operation === 'createFolder') {
|
||||
// ----------------------------------
|
||||
// createFolder
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
isJson = true;
|
||||
body = {
|
||||
path: this.getNodeParameter('path', i) as string,
|
||||
};
|
||||
|
||||
endpoint = 'https://api.dropboxapi.com/2/files/create_folder_v2';
|
||||
|
||||
} else if (operation === 'delete') {
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
isJson = true;
|
||||
body = {
|
||||
path: this.getNodeParameter('path', i) as string,
|
||||
};
|
||||
|
||||
endpoint = 'https://api.dropboxapi.com/2/files/delete_v2';
|
||||
|
||||
} else if (operation === 'downloadFile') {
|
||||
// ----------------------------------
|
||||
// downloadFile
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
headers['Dropbox-API-Arg'] = JSON.stringify({
|
||||
path: this.getNodeParameter('path', i) as string,
|
||||
});
|
||||
|
||||
endpoint = 'https://content.dropboxapi.com/2/files/download';
|
||||
|
||||
} else if (operation === 'listFolderContent') {
|
||||
// ----------------------------------
|
||||
// listFolderContent
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
isJson = true;
|
||||
body = {
|
||||
path: this.getNodeParameter('path', i) as string,
|
||||
limit: 2000,
|
||||
};
|
||||
|
||||
// TODO: If more files than the max-amount exist it has to be possible to
|
||||
// also request them.
|
||||
|
||||
endpoint = 'https://api.dropboxapi.com/2/files/list_folder';
|
||||
|
||||
} else if (operation === 'move') {
|
||||
// ----------------------------------
|
||||
// move
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
isJson = true;
|
||||
body = {
|
||||
from_path: this.getNodeParameter('path', i) as string,
|
||||
to_path: this.getNodeParameter('toPath', i) as string,
|
||||
};
|
||||
|
||||
endpoint = 'https://api.dropboxapi.com/2/files/move_v2';
|
||||
|
||||
} else if (operation === 'uploadFile') {
|
||||
// ----------------------------------
|
||||
// uploadFile
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
headers['Dropbox-API-Arg'] = JSON.stringify({
|
||||
mode: 'overwrite',
|
||||
path: this.getNodeParameter('path', i) as string,
|
||||
});
|
||||
|
||||
endpoint = 'https://content.dropboxapi.com/2/files/upload';
|
||||
|
||||
if (this.getNodeParameter('binaryData', i) === true) {
|
||||
// Is binary file to upload
|
||||
const item = items[i];
|
||||
|
||||
if (item.binary === undefined) {
|
||||
throw new Error('No binary data exists on item!');
|
||||
}
|
||||
|
||||
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||
|
||||
|
||||
if (item.binary[propertyNameUpload] === undefined) {
|
||||
throw new Error(`No binary data property "${propertyNameUpload}" does not exists on item!`);
|
||||
}
|
||||
|
||||
body = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING);
|
||||
} else {
|
||||
// Is text file
|
||||
body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not known!`);
|
||||
}
|
||||
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers,
|
||||
method: requestMethod,
|
||||
qs: {},
|
||||
uri: endpoint,
|
||||
json: isJson,
|
||||
};
|
||||
|
||||
if (Object.keys(body).length) {
|
||||
options.body = body;
|
||||
}
|
||||
|
||||
if (operation === 'downloadFile') {
|
||||
// Return the data as a buffer
|
||||
options.encoding = null;
|
||||
}
|
||||
|
||||
const responseData = await this.helpers.request(options);
|
||||
|
||||
if (operation === 'downloadFile') {
|
||||
// TODO: Has to check if it already exists and only add if not
|
||||
if (items[i].binary === undefined) {
|
||||
items[i].binary = {};
|
||||
}
|
||||
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||
|
||||
const filePathDownload = this.getNodeParameter('path', i) as string;
|
||||
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(responseData, filePathDownload);
|
||||
|
||||
} else if (operation === 'listFolderContent') {
|
||||
|
||||
const propNames: { [key: string]: string } = {
|
||||
'id': 'id',
|
||||
'name': 'name',
|
||||
'client_modified': 'lastModifiedClient',
|
||||
'server_modified': 'lastModifiedServer',
|
||||
'rev': 'rev',
|
||||
'size': 'contentSize',
|
||||
'.tag': 'type',
|
||||
'content_hash': 'contentHash',
|
||||
};
|
||||
|
||||
for (const item of responseData.entries) {
|
||||
const newItem: IDataObject = {};
|
||||
|
||||
// Get the props and save them under a proper name
|
||||
for (const propName of Object.keys(propNames)) {
|
||||
if (item[propName] !== undefined) {
|
||||
newItem[propNames[propName]] = item[propName];
|
||||
}
|
||||
}
|
||||
|
||||
returnData.push(newItem as IDataObject);
|
||||
}
|
||||
} else {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'downloadFile') {
|
||||
// For file downloads the files get attached to the existing items
|
||||
return this.prepareOutputData(items);
|
||||
} else {
|
||||
// For all other ones does the output get replaced
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Dropbox/dropbox.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
554
packages/nodes-base/nodes/EditImage.node.ts
Normal file
@@ -0,0 +1,554 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteSingleFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import * as gm from 'gm';
|
||||
|
||||
|
||||
export class EditImage implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Edit Image',
|
||||
name: 'editImage',
|
||||
icon: 'fa:image',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Edits an image like blur, resize or adding border and text',
|
||||
defaults: {
|
||||
name: 'Edit Image',
|
||||
color: '#553399',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Blur',
|
||||
value: 'blur',
|
||||
description: 'Adds a blur to the image and so makes it less sharp',
|
||||
},
|
||||
{
|
||||
name: 'Border',
|
||||
value: 'border',
|
||||
description: 'Adds a border to the image',
|
||||
},
|
||||
{
|
||||
name: 'Crop',
|
||||
value: 'crop',
|
||||
description: 'Crops the image',
|
||||
},
|
||||
{
|
||||
name: 'Get Information',
|
||||
value: 'information',
|
||||
description: 'Returns image information like resolution',
|
||||
},
|
||||
{
|
||||
name: 'Rotate',
|
||||
value: 'rotate',
|
||||
description: 'Rotate image',
|
||||
},
|
||||
{
|
||||
name: 'Resize',
|
||||
value: 'resize',
|
||||
description: 'Change the size of image',
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
description: 'Adds text to image',
|
||||
},
|
||||
],
|
||||
default: 'border',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
description: 'Name of the binary property in which the image data can be found.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// text
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'Text to render',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'text'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Text to write on the image.',
|
||||
},
|
||||
{
|
||||
displayName: 'Font Size',
|
||||
name: 'fontSize',
|
||||
type: 'number',
|
||||
default: 18,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'text'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Size of the text.',
|
||||
},
|
||||
{
|
||||
displayName: 'Font Color',
|
||||
name: 'fontColor',
|
||||
type: 'color',
|
||||
default: '#000000',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'text'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Color of the text.',
|
||||
},
|
||||
{
|
||||
displayName: 'Position X',
|
||||
name: 'positionX',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'text'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'X (horizontal) position of the text.',
|
||||
},
|
||||
{
|
||||
displayName: 'Position Y',
|
||||
name: 'positionY',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'text'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Y (vertical) position of the text.',
|
||||
},
|
||||
{
|
||||
displayName: 'Max Line Length',
|
||||
name: 'lineLength',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 80,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'text'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Max amount of characters in a line before a<br />line-break should get added.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// blur
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Blur',
|
||||
name: 'blur',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 1000,
|
||||
},
|
||||
default: 5,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'blur'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'How strong the blur should be',
|
||||
},
|
||||
{
|
||||
displayName: 'Sigma',
|
||||
name: 'sigma',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 1000,
|
||||
},
|
||||
default: 2,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'blur'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The sigma of the blur',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// border
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Border Width',
|
||||
name: 'borderWidth',
|
||||
type: 'number',
|
||||
default: 10,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'border'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The width of the border',
|
||||
},
|
||||
{
|
||||
displayName: 'Border Height',
|
||||
name: 'borderHeight',
|
||||
type: 'number',
|
||||
default: 10,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'border'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The height of the border',
|
||||
},
|
||||
{
|
||||
displayName: 'Border Color',
|
||||
name: 'borderColor',
|
||||
type: 'color',
|
||||
default: '#000000',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'border'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Color of the border.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// crop
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Width',
|
||||
name: 'width',
|
||||
type: 'number',
|
||||
default: 500,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'crop'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Crop width',
|
||||
},
|
||||
{
|
||||
displayName: 'Height',
|
||||
name: 'height',
|
||||
type: 'number',
|
||||
default: 500,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'crop'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Crop height',
|
||||
},
|
||||
{
|
||||
displayName: 'Position X',
|
||||
name: 'positionX',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'crop'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'X (horizontal) position to crop from.',
|
||||
},
|
||||
{
|
||||
displayName: 'Position Y',
|
||||
name: 'positionY',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'crop'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Y (vertical) position to crop from.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// resize
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Width',
|
||||
name: 'width',
|
||||
type: 'number',
|
||||
default: 500,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'resize'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'New width of the image',
|
||||
},
|
||||
{
|
||||
displayName: 'Height',
|
||||
name: 'height',
|
||||
type: 'number',
|
||||
default: 500,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'resize'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'New height of the image',
|
||||
},
|
||||
{
|
||||
displayName: 'Option',
|
||||
name: 'resizeOption',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Ignore Aspect Ratio',
|
||||
value: 'ignoreAspectRatio',
|
||||
description: 'Ignore aspect ratio and resize exactly to specified values',
|
||||
},
|
||||
{
|
||||
name: 'Maximum area',
|
||||
value: 'maximumArea',
|
||||
description: 'Specified values are maximum area',
|
||||
},
|
||||
{
|
||||
name: 'Minimum Area',
|
||||
value: 'minimumArea',
|
||||
description: 'Specified values are minimum area',
|
||||
},
|
||||
{
|
||||
name: 'Only if larger',
|
||||
value: 'onlyIfLarger',
|
||||
description: 'Resize only if image is larger than width or height',
|
||||
},
|
||||
{
|
||||
name: 'Only if smaller',
|
||||
value: 'onlyIfSmaller',
|
||||
description: 'Resize only if image is smaller than width or height',
|
||||
},
|
||||
],
|
||||
default: 'maximumArea',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'resize'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'How to resize the image.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// rotate
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Rotate',
|
||||
name: 'rotate',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: -360,
|
||||
maxValue: 360,
|
||||
},
|
||||
default: 0,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'rotate'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'How much the image should be rotated',
|
||||
},
|
||||
{
|
||||
displayName: 'Background Color',
|
||||
name: 'backgroundColor',
|
||||
type: 'color',
|
||||
default: '#ffffff',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'rotate'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The color to use for the background when image gets rotated by anything which is not a multiple of 90..',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const item = this.getInputData();
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
const dataPropertyName = this.getNodeParameter('dataPropertyName') as string;
|
||||
|
||||
// TODO: Later should make so that it sends directly a valid buffer and the buffer.from stuff is not needed anymore
|
||||
if (item.binary === undefined) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (item.binary[dataPropertyName as string] === undefined) {
|
||||
return item;
|
||||
}
|
||||
|
||||
let gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING));
|
||||
|
||||
if (operation === 'blur') {
|
||||
const blur = this.getNodeParameter('blur') as number;
|
||||
const sigma = this.getNodeParameter('sigma') as number;
|
||||
gmInstance = gmInstance.blur(blur, sigma);
|
||||
} else if (operation === 'border') {
|
||||
const borderWidth = this.getNodeParameter('borderWidth') as number;
|
||||
const borderHeight = this.getNodeParameter('borderHeight') as number;
|
||||
const borderColor = this.getNodeParameter('borderColor') as string;
|
||||
|
||||
gmInstance = gmInstance.borderColor(borderColor).border(borderWidth, borderHeight);
|
||||
} else if (operation === 'crop') {
|
||||
const width = this.getNodeParameter('width') as number;
|
||||
const height = this.getNodeParameter('height') as number;
|
||||
|
||||
const positionX = this.getNodeParameter('positionX') as number;
|
||||
const positionY = this.getNodeParameter('positionY') as number;
|
||||
|
||||
gmInstance = gmInstance.crop(width, height, positionX, positionY);
|
||||
} else if (operation === 'information') {
|
||||
const imageData = await new Promise<IDataObject>((resolve, reject) => {
|
||||
gmInstance = gmInstance.identify((error, imageData) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(imageData as unknown as IDataObject);
|
||||
});
|
||||
});
|
||||
|
||||
item.json = imageData;
|
||||
} else if (operation === 'resize') {
|
||||
const width = this.getNodeParameter('width') as number;
|
||||
const height = this.getNodeParameter('height') as number;
|
||||
const resizeOption = this.getNodeParameter('resizeOption') as string;
|
||||
|
||||
// By default use "maximumArea"
|
||||
let option: gm.ResizeOption = '@';
|
||||
if (resizeOption === 'ignoreAspectRatio') {
|
||||
option = '!';
|
||||
} else if (resizeOption === 'minimumArea') {
|
||||
option = '^';
|
||||
} else if (resizeOption === 'onlyIfSmaller') {
|
||||
option = '<';
|
||||
} else if (resizeOption === 'onlyIfLarger') {
|
||||
option = '>';
|
||||
}
|
||||
|
||||
gmInstance = gmInstance.resize(width, height, option);
|
||||
} else if (operation === 'rotate') {
|
||||
const rotate = this.getNodeParameter('rotate') as number;
|
||||
const backgroundColor = this.getNodeParameter('backgroundColor') as string;
|
||||
gmInstance = gmInstance.rotate(backgroundColor, rotate);
|
||||
} else if (operation === 'text') {
|
||||
const fontColor = this.getNodeParameter('fontColor') as string;
|
||||
const fontSize = this.getNodeParameter('fontSize') as number;
|
||||
const lineLength = this.getNodeParameter('lineLength') as number;
|
||||
const positionX = this.getNodeParameter('positionX') as number;
|
||||
const positionY = this.getNodeParameter('positionY') as number;
|
||||
const text = this.getNodeParameter('text') as string;
|
||||
|
||||
// Split the text in multiple lines
|
||||
const lines: string[] = [];
|
||||
let currentLine = '';
|
||||
(text as string).split(' ').forEach((textPart: string) => {
|
||||
if (currentLine.length + textPart.length + 1 > lineLength) {
|
||||
lines.push(currentLine.trim());
|
||||
currentLine = `${textPart} `;
|
||||
return;
|
||||
}
|
||||
currentLine += `${textPart} `;
|
||||
});
|
||||
// Add the last line
|
||||
lines.push(currentLine.trim());
|
||||
|
||||
// Combine the lines to a single string
|
||||
const renderText = lines.join('\n');
|
||||
|
||||
gmInstance = gmInstance
|
||||
.fill(fontColor)
|
||||
.fontSize(fontSize)
|
||||
.drawText(positionX, positionY, renderText);
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not supported!`);
|
||||
}
|
||||
|
||||
return new Promise<INodeExecutionData>((resolve, reject) => {
|
||||
gmInstance
|
||||
.toBuffer((error: Error, buffer: Buffer) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
item.binary![dataPropertyName as string].data = buffer.toString(BINARY_ENCODING);
|
||||
|
||||
return resolve(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
261
packages/nodes-base/nodes/EmailReadImap.node.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { ITriggerFunctions } from 'n8n-core';
|
||||
import {
|
||||
IBinaryData,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ITriggerResponse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { connect as imapConnect, ImapSimple, getParts, Message } from 'imap-simple';
|
||||
|
||||
export class EmailReadImap implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'EmailReadImap',
|
||||
name: 'emailReadImap',
|
||||
icon: 'fa:inbox',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Triggers the workflow when a new email gets received',
|
||||
defaults: {
|
||||
name: 'IMAP Email',
|
||||
color: '#44AA22',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'imap',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Mailbox Name',
|
||||
name: 'mailbox',
|
||||
type: 'string',
|
||||
default: 'INBOX',
|
||||
},
|
||||
{
|
||||
displayName: 'Action',
|
||||
name: 'postProcessAction',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Mark as read',
|
||||
value: 'read'
|
||||
},
|
||||
{
|
||||
name: 'Nothing',
|
||||
value: 'nothing'
|
||||
},
|
||||
],
|
||||
default: 'read',
|
||||
description: 'What to do after the email has been received. If "nothing" gets<br />selected it will be processed multiple times.',
|
||||
},
|
||||
{
|
||||
displayName: 'Download Attachments',
|
||||
name: 'downloadAttachments',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'If attachments of emails should be downloaded.<br />Only set if needed as it increases processing.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Prefix Name',
|
||||
name: 'dataPropertyAttachmentsPrefixName',
|
||||
type: 'string',
|
||||
default: 'attachment_',
|
||||
displayOptions: {
|
||||
show: {
|
||||
downloadAttachments: [
|
||||
'true'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />So if name is "attachment_" the first attachment is saved to "attachment_0"',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
|
||||
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||
let isFirstRun = true;
|
||||
|
||||
const credentials = this.getCredentials('imap');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const mailbox = this.getNodeParameter('mailbox') as string;
|
||||
const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
|
||||
const postProcessAction = this.getNodeParameter('postProcessAction') as string;
|
||||
const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean;
|
||||
|
||||
|
||||
// Returns the email text
|
||||
const getText = async (parts: any[], message: Message, subtype: string) => { // tslint:disable-line:no-any
|
||||
if (!message.attributes.struct) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const textParts = parts.filter((part) => {
|
||||
return part.type.toUpperCase() === 'TEXT' && part.subtype.toUpperCase() === subtype.toUpperCase();
|
||||
});
|
||||
|
||||
if (textParts.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return await connection.getPartData(message, textParts[0]);
|
||||
};
|
||||
|
||||
|
||||
// Returns the email attachments
|
||||
const getAttachment = async (connection: ImapSimple, parts: any[], message: Message): Promise<IBinaryData[]> => { // tslint:disable-line:no-any
|
||||
if (!message.attributes.struct) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the message has attachments and if so get them
|
||||
const attachmentParts = parts.filter((part) => {
|
||||
return part.disposition && part.disposition.type.toUpperCase() === 'ATTACHMENT';
|
||||
});
|
||||
|
||||
const attachmentPromises = [];
|
||||
let attachmentPromise;
|
||||
for (const attachmentPart of attachmentParts) {
|
||||
attachmentPromise = connection.getPartData(message, attachmentPart)
|
||||
.then((partData) => {
|
||||
// Return it in the format n8n expects
|
||||
return this.helpers.prepareBinaryData(partData, attachmentPart.disposition.params.filename);
|
||||
});
|
||||
|
||||
attachmentPromises.push(attachmentPromise);
|
||||
}
|
||||
|
||||
return Promise.all(attachmentPromises);
|
||||
};
|
||||
|
||||
|
||||
// Returns all the new unseen messages
|
||||
const getNewEmails = async (connection: ImapSimple): Promise<INodeExecutionData[]> => {
|
||||
|
||||
const searchCriteria = [
|
||||
'UNSEEN'
|
||||
];
|
||||
|
||||
const fetchOptions = {
|
||||
bodies: ['HEADER', 'TEXT'],
|
||||
markSeen: postProcessAction === 'read',
|
||||
struct: true,
|
||||
};
|
||||
|
||||
const results = await connection.search(searchCriteria, fetchOptions);
|
||||
|
||||
const newEmails: INodeExecutionData[] = [];
|
||||
let newEmail: INodeExecutionData, messageHeader, messageBody;
|
||||
let attachments: IBinaryData[];
|
||||
let propertyName: string;
|
||||
|
||||
// All properties get by default moved to metadata except the ones
|
||||
// which are defined here which get set on the top level.
|
||||
const topLevelProperties = [
|
||||
'cc',
|
||||
'date',
|
||||
'from',
|
||||
'subject',
|
||||
'to',
|
||||
];
|
||||
for (const message of results) {
|
||||
|
||||
const parts = getParts(message.attributes.struct!);
|
||||
|
||||
newEmail = {
|
||||
json: {
|
||||
textHtml: await getText(parts, message, 'html'),
|
||||
textPlain: await getText(parts, message, 'plain'),
|
||||
metadata: {} as IDataObject,
|
||||
}
|
||||
};
|
||||
|
||||
messageHeader = message.parts.filter((part) => {
|
||||
return part.which === 'HEADER';
|
||||
});
|
||||
|
||||
messageBody = messageHeader[0].body;
|
||||
for (propertyName of Object.keys(messageBody)) {
|
||||
if (messageBody[propertyName].length) {
|
||||
if (topLevelProperties.includes(propertyName)) {
|
||||
newEmail.json[propertyName] = messageBody[propertyName][0];
|
||||
} else {
|
||||
(newEmail.json.metadata as IDataObject)[propertyName] = messageBody[propertyName][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadAttachments === true) {
|
||||
// Get attachments and add them if any get found
|
||||
attachments = await getAttachment(connection, parts, message);
|
||||
if (attachments.length) {
|
||||
newEmail.binary = {};
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newEmails.push(newEmail);
|
||||
}
|
||||
|
||||
return newEmails;
|
||||
};
|
||||
|
||||
|
||||
|
||||
let connection: ImapSimple;
|
||||
|
||||
const config = {
|
||||
imap: {
|
||||
user: credentials.user as string,
|
||||
password: credentials.password as string,
|
||||
host: credentials.host as string,
|
||||
port: credentials.port as number,
|
||||
tls: credentials.secure as boolean,
|
||||
authTimeout: 3000
|
||||
},
|
||||
onmail: async () => {
|
||||
const returnData = await getNewEmails(connection);
|
||||
|
||||
if (returnData.length) {
|
||||
this.emit([returnData]);
|
||||
} else if (isFirstRun === true && this.getMode() === 'manual') {
|
||||
// If it is the first run we emit even when it is empty. If we would
|
||||
// not do that it would wait till the first unread email arrives
|
||||
// before it would continue to execute the next node.
|
||||
this.emit([]);
|
||||
}
|
||||
|
||||
isFirstRun = false;
|
||||
},
|
||||
};
|
||||
|
||||
// Connect to the IMAP server and open the mailbox
|
||||
// that we get informed whenever a new email arrives
|
||||
connection = await imapConnect(config);
|
||||
await connection.openBox(mailbox);
|
||||
|
||||
|
||||
// When workflow and so node gets set to inactive close the connectoin
|
||||
async function closeFunction() {
|
||||
await connection.end();
|
||||
}
|
||||
|
||||
return {
|
||||
closeFunction,
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
158
packages/nodes-base/nodes/EmailSend.node.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteSingleFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { createTransport } from 'nodemailer';
|
||||
|
||||
|
||||
export class EmailSend implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Send Email',
|
||||
name: 'emailSend',
|
||||
icon: 'fa:envelope',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
description: 'Sends an Email',
|
||||
defaults: {
|
||||
name: 'Send Email',
|
||||
color: '#44DD22',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'smtp',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
// TODO: Add cc, bcc and choice for text as text or html (maybe also from name)
|
||||
{
|
||||
displayName: 'From Email',
|
||||
name: 'fromEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'admin@example.com',
|
||||
description: 'Email address of the sender optional with name.',
|
||||
},
|
||||
{
|
||||
displayName: 'To Email',
|
||||
name: 'toEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'info@example.com',
|
||||
description: 'Email address of the recipient.',
|
||||
},
|
||||
{
|
||||
displayName: 'Subject',
|
||||
name: 'subject',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'My subject line',
|
||||
description: 'Subject line of the email.',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
rows: 5,
|
||||
},
|
||||
default: '',
|
||||
description: 'Plain text message of email.',
|
||||
},
|
||||
{
|
||||
displayName: 'HTML',
|
||||
name: 'html',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
default: '',
|
||||
description: 'HTML text message of email.',
|
||||
},
|
||||
{
|
||||
displayName: 'Attachments',
|
||||
name: 'attachments',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the binary properties which contain<br />data which should be added to email as attachment.<br />Multiple ones can be comma separated.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const item = this.getInputData();
|
||||
|
||||
const fromEmail = this.getNodeParameter('fromEmail') as string;
|
||||
const toEmail = this.getNodeParameter('toEmail') as string;
|
||||
const subject = this.getNodeParameter('subject') as string;
|
||||
const text = this.getNodeParameter('text') as string;
|
||||
const html = this.getNodeParameter('html') as string;
|
||||
const attachmentPropertyString = this.getNodeParameter('attachments') as string;
|
||||
|
||||
const credentials = this.getCredentials('smtp');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const transporter = createTransport({
|
||||
// @ts-ignore
|
||||
host: credentials.host as string,
|
||||
port: credentials.port as number,
|
||||
secure: credentials.secure as boolean,
|
||||
auth: {
|
||||
user: credentials.user,
|
||||
pass: credentials.password,
|
||||
}
|
||||
});
|
||||
|
||||
// setup email data with unicode symbols
|
||||
const mailOptions = {
|
||||
from: fromEmail,
|
||||
to: toEmail,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
};
|
||||
|
||||
if (attachmentPropertyString && item.binary) {
|
||||
const attachments = [];
|
||||
const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => {
|
||||
return propertyName.trim();
|
||||
});
|
||||
|
||||
for (const propertyName of attachmentProperties) {
|
||||
if (!item.binary.hasOwnProperty(propertyName)) {
|
||||
continue;
|
||||
}
|
||||
attachments.push({
|
||||
filename: item.binary[propertyName].fileName || 'unknown',
|
||||
content: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING),
|
||||
});
|
||||
}
|
||||
|
||||
if (attachments.length) {
|
||||
// @ts-ignore
|
||||
mailOptions.attachments = attachments;
|
||||
}
|
||||
}
|
||||
|
||||
// Send the email
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
|
||||
return { json: info };
|
||||
}
|
||||
|
||||
}
|
||||
54
packages/nodes-base/nodes/ErrorTrigger.node.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class ErrorTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Error Trigger',
|
||||
name: 'errorTrigger',
|
||||
icon: 'fa:bug',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Triggers the workflow when another workflow has an error',
|
||||
maxNodes: 1,
|
||||
defaults: {
|
||||
name: 'Error Trigger',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: []
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
const mode = this.getMode();
|
||||
|
||||
if (mode === 'manual' && items.length === 1 && Object.keys(items[0].json).length === 0 && items[0].binary === undefined) {
|
||||
// If we are in manual mode and no input data got provided we return
|
||||
// example data to allow to develope and test errorWorkflows easily
|
||||
items[0].json = {
|
||||
execution: {
|
||||
error: {
|
||||
message: 'Example Error Message',
|
||||
stack: 'Stacktrace'
|
||||
},
|
||||
lastNodeExecuted: '',
|
||||
mode: 'manual'
|
||||
},
|
||||
workflow: {
|
||||
id: '1',
|
||||
name: 'Example Workflow'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
117
packages/nodes-base/nodes/ExecuteCommand.node.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { exec } from 'child_process';
|
||||
|
||||
|
||||
export interface IExecReturnData {
|
||||
exitCode: number;
|
||||
error?: Error;
|
||||
stderr: string;
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Promisifiy exec manually to also get the exit code
|
||||
*
|
||||
* @param {string} command
|
||||
* @returns {Promise<IExecReturnData>}
|
||||
*/
|
||||
function execPromise(command: string): Promise<IExecReturnData> {
|
||||
const returnData: IExecReturnData = {
|
||||
exitCode: 0,
|
||||
stderr: '',
|
||||
stdout: '',
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
returnData.stdout = stdout.trim();
|
||||
returnData.stderr = stderr.trim();
|
||||
|
||||
if (error) {
|
||||
returnData.error = error;
|
||||
}
|
||||
|
||||
resolve(returnData);
|
||||
}).on('exit', code => { returnData.exitCode = code || 0; });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export class ExecuteCommand implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Execute Command',
|
||||
name: 'executeCommand',
|
||||
icon: 'fa:terminal',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Executes a command on the host.',
|
||||
defaults: {
|
||||
name: 'Execute Command',
|
||||
color: '#886644',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Execute Once',
|
||||
name: 'executeOnce',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'If activated it executes only once instead of once for each entry.',
|
||||
},
|
||||
{
|
||||
displayName: 'Command',
|
||||
name: 'command',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'echo "test"',
|
||||
description: 'The command to execute',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
|
||||
let items = this.getInputData();
|
||||
|
||||
let command: string;
|
||||
const executeOnce = this.getNodeParameter('executeOnce', 0) as boolean;
|
||||
|
||||
if (executeOnce === true) {
|
||||
items = [items[0]];
|
||||
}
|
||||
|
||||
let item: INodeExecutionData;
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
item = items[itemIndex];
|
||||
|
||||
command = this.getNodeParameter('command', itemIndex) as string;
|
||||
|
||||
const {
|
||||
// error, TODO: Later make it possible to select if it should fail on error or not
|
||||
exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
} = await execPromise(command);
|
||||
|
||||
item.json = {
|
||||
exitCode,
|
||||
stderr,
|
||||
stdout,
|
||||
};
|
||||
}
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
71
packages/nodes-base/nodes/Function.node.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
const { NodeVM } = require('vm2');
|
||||
|
||||
export class Function implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Function',
|
||||
name: 'function',
|
||||
icon: 'fa:code',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Run custom function code which gets executed once per item.',
|
||||
defaults: {
|
||||
name: 'Function',
|
||||
color: '#FF9922',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Function',
|
||||
name: 'functionCode',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
rows: 10,
|
||||
},
|
||||
type: 'string',
|
||||
default: 'items[0].json.myVariable = 1;\nreturn items;',
|
||||
description: 'The JavaScript code to execute.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
// const item = this.getInputData();
|
||||
let items = this.getInputData();
|
||||
|
||||
// Define the global objects for the custom function
|
||||
const sandbox = {
|
||||
getNodeParameter: this.getNodeParameter,
|
||||
helpers: this.helpers,
|
||||
items,
|
||||
};
|
||||
|
||||
const vm = new NodeVM({
|
||||
console: 'inherit',
|
||||
sandbox,
|
||||
require: {
|
||||
external: false,
|
||||
root: './',
|
||||
}
|
||||
});
|
||||
|
||||
// Get the code to execute
|
||||
const functionCode = this.getNodeParameter('functionCode', 0) as string;
|
||||
|
||||
try {
|
||||
// Execute the function code
|
||||
items = await vm.run(`module.exports = async function() {${functionCode}}()`);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
82
packages/nodes-base/nodes/FunctionItem.node.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { IExecuteSingleFunctions } from 'n8n-core';
|
||||
import {
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
const { NodeVM } = require('vm2');
|
||||
|
||||
export class FunctionItem implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Function Item',
|
||||
name: 'functionItem',
|
||||
icon: 'fa:code',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Run custom function code which gets executed once per item.',
|
||||
defaults: {
|
||||
name: 'FunctionItem',
|
||||
color: '#FF9922',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Function',
|
||||
name: 'functionCode',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
rows: 10,
|
||||
},
|
||||
type: 'string',
|
||||
default: 'item.myVariable = 1;\nreturn item;',
|
||||
description: 'The JavaScript code to execute for each item.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const item = this.getInputData();
|
||||
|
||||
// Define the global objects for the custom function
|
||||
const sandbox = {
|
||||
getBinaryData: (): IBinaryKeyData | undefined => {
|
||||
return item.binary;
|
||||
},
|
||||
getNodeParameter: this.getNodeParameter,
|
||||
helpers: this.helpers,
|
||||
item: item.json,
|
||||
setBinaryData: (data: IBinaryKeyData) => {
|
||||
item.binary = data;
|
||||
},
|
||||
};
|
||||
|
||||
const vm = new NodeVM({
|
||||
console: 'inherit',
|
||||
sandbox,
|
||||
require: {
|
||||
external: false,
|
||||
root: './',
|
||||
}
|
||||
});
|
||||
|
||||
// Get the code to execute
|
||||
const functionCode = this.getNodeParameter('functionCode') as string;
|
||||
|
||||
|
||||
let jsonData: IDataObject;
|
||||
try {
|
||||
// Execute the function code
|
||||
jsonData = await vm.run(`module.exports = async function() {${functionCode}}()`);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
return {
|
||||
json: jsonData
|
||||
};
|
||||
}
|
||||
}
|
||||
80
packages/nodes-base/nodes/Github/GenericFunctions.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Make an API request to Github
|
||||
*
|
||||
* @param {IHookFunctions} this
|
||||
* @param {string} method
|
||||
* @param {string} url
|
||||
* @param {object} body
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('githubApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `token ${credentials.accessToken}`,
|
||||
'User-Agent': credentials.user,
|
||||
},
|
||||
body,
|
||||
qs: query,
|
||||
uri: `https://api.github.com${endpoint}`,
|
||||
json: true
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.request(options);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 401) {
|
||||
// Return a clear error
|
||||
throw new Error('The Github credentials are not valid!');
|
||||
}
|
||||
|
||||
if (error.response && error.response.body && error.response.body.message) {
|
||||
// Try to return the error prettier
|
||||
throw new Error(`Github error response [${error.statusCode}]: ${error.response.body.message}`);
|
||||
}
|
||||
|
||||
// If that data does not exist for some reason return the actual error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the SHA of the given file
|
||||
*
|
||||
* @export
|
||||
* @param {(IHookFunctions | IExecuteFunctions)} this
|
||||
* @param {string} owner
|
||||
* @param {string} repository
|
||||
* @param {string} filePath
|
||||
* @param {string} [branch]
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function getFileSha(this: IHookFunctions | IExecuteFunctions, owner: string, repository: string, filePath: string, branch?: string): Promise<any> { // tslint:disable-line:no-any
|
||||
const getBody: IDataObject = {};
|
||||
if (branch !== undefined) {
|
||||
getBody.branch = branch;
|
||||
}
|
||||
const getEndpoint = `/repos/${owner}/${repository}/contents/${encodeURI(filePath)}`;
|
||||
const responseData = await githubApiRequest.call(this, 'GET', getEndpoint, getBody, {});
|
||||
|
||||
if (responseData.sha === undefined) {
|
||||
throw new Error('Could not get the SHA of the file.');
|
||||
}
|
||||
return responseData.sha;
|
||||
}
|
||||
1152
packages/nodes-base/nodes/Github/Github.node.ts
Normal file
427
packages/nodes-base/nodes/Github/GithubTrigger.node.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
import {
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeType,
|
||||
IWebhookResonseData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
githubApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
|
||||
export class GithubTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Github Trigger',
|
||||
name: 'githubTrigger',
|
||||
icon: 'file:github.png',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when a Github events occurs.',
|
||||
defaults: {
|
||||
name: 'Github Trigger',
|
||||
color: '#885577',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'githubApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
reponseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Repository Owner',
|
||||
name: 'owner',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'n8n-io',
|
||||
description: 'Owner of the repsitory.',
|
||||
},
|
||||
{
|
||||
displayName: 'Repository Name',
|
||||
name: 'repository',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'n8n',
|
||||
description: 'The name of the repsitory.',
|
||||
},
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{
|
||||
name: '*',
|
||||
value: '*',
|
||||
description: 'Any time any event is triggered (Wildcard Event).',
|
||||
},
|
||||
{
|
||||
name: 'check_run',
|
||||
value: 'check_run',
|
||||
description: 'Triggered when a check run is created, rerequested, completed, or has a requested_action.',
|
||||
},
|
||||
{
|
||||
name: 'check_suite',
|
||||
value: 'check_suite',
|
||||
description: 'Triggered when a check suite is completed, requested, or rerequested.',
|
||||
},
|
||||
{
|
||||
name: 'commit_comment',
|
||||
value: 'commit_comment',
|
||||
description: 'Triggered when a commit comment is created.',
|
||||
},
|
||||
{
|
||||
name: 'content_reference',
|
||||
value: 'content_reference',
|
||||
description: 'Triggered when the body or comment of an issue or pull request includes a URL that matches a configured content reference domain. Only GitHub Apps can receive this event.',
|
||||
},
|
||||
{
|
||||
name: 'create',
|
||||
value: 'create',
|
||||
description: 'Represents a created repository, branch, or tag.',
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
value: 'delete',
|
||||
description: 'Represents a deleted branch or tag.',
|
||||
},
|
||||
{
|
||||
name: 'deploy_key',
|
||||
value: 'deploy_key',
|
||||
description: 'Triggered when a deploy key is added or removed from a repository.',
|
||||
},
|
||||
{
|
||||
name: 'deployment',
|
||||
value: 'deployment',
|
||||
description: 'Represents a deployment.',
|
||||
},
|
||||
{
|
||||
name: 'deployment_status',
|
||||
value: 'deployment_status',
|
||||
description: 'Represents a deployment status.',
|
||||
},
|
||||
{
|
||||
name: 'fork',
|
||||
value: 'fork',
|
||||
description: 'Triggered when a user forks a repository.',
|
||||
},
|
||||
{
|
||||
name: 'github_app_authorization',
|
||||
value: 'github_app_authorization',
|
||||
description: 'Triggered when someone revokes their authorization of a GitHub App.',
|
||||
},
|
||||
{
|
||||
name: 'gollum',
|
||||
value: 'gollum',
|
||||
description: 'Triggered when a Wiki page is created or updated.',
|
||||
},
|
||||
{
|
||||
name: 'installation',
|
||||
value: 'installation',
|
||||
description: 'Triggered when someone installs (created) , uninstalls (deleted), or accepts new permissions (new_permissions_accepted) for a GitHub App. When a GitHub App owner requests new permissions, the person who installed the GitHub App must accept the new permissions request.',
|
||||
},
|
||||
{
|
||||
name: 'installation_repositories',
|
||||
value: 'installation_repositories',
|
||||
description: 'Triggered when a repository is added or removed from an installation.',
|
||||
},
|
||||
{
|
||||
name: 'issue_comment',
|
||||
value: 'issue_comment',
|
||||
description: 'Triggered when an issue comment is created, edited, or deleted.',
|
||||
},
|
||||
{
|
||||
name: 'issues',
|
||||
value: 'issues',
|
||||
description: 'Triggered when an issue is opened, edited, deleted, transferred, pinned, unpinned, closed, reopened, assigned, unassigned, labeled, unlabeled, locked, unlocked, milestoned, or demilestoned.',
|
||||
},
|
||||
{
|
||||
name: 'label',
|
||||
value: 'label',
|
||||
description: 'Triggered when a repository\'s label is created, edited, or deleted.',
|
||||
},
|
||||
{
|
||||
name: 'marketplace_purchase',
|
||||
value: 'marketplace_purchase',
|
||||
description: 'Triggered when someone purchases a GitHub Marketplace plan, cancels their plan, upgrades their plan (effective immediately), downgrades a plan that remains pending until the end of the billing cycle, or cancels a pending plan change.',
|
||||
},
|
||||
{
|
||||
name: 'member',
|
||||
value: 'member',
|
||||
description: 'Triggered when a user accepts an invitation or is removed as a collaborator to a repository, or has their permissions changed.',
|
||||
},
|
||||
{
|
||||
name: 'membership',
|
||||
value: 'membership',
|
||||
description: 'Triggered when a user is added or removed from a team. Organization hooks only.',
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
value: 'meta',
|
||||
description: 'Triggered when the webhook that this event is configured on is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'milestone',
|
||||
value: 'milestone',
|
||||
description: 'Triggered when a milestone is created, closed, opened, edited, or deleted.',
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
value: 'organization',
|
||||
description: 'Triggered when an organization is deleted and renamed, and when a user is added, removed, or invited to an organization. Organization hooks only.',
|
||||
},
|
||||
{
|
||||
name: 'org_block',
|
||||
value: 'org_block',
|
||||
description: 'Triggered when an organization blocks or unblocks a user. Organization hooks only.',
|
||||
},
|
||||
{
|
||||
name: 'page_build',
|
||||
value: 'page_build',
|
||||
description: 'Triggered on push to a GitHub Pages enabled branch (gh-pages for project pages, master for user and organization pages).',
|
||||
},
|
||||
{
|
||||
name: 'project_card',
|
||||
value: 'project_card',
|
||||
description: 'Triggered when a project card is created, edited, moved, converted to an issue, or deleted.',
|
||||
},
|
||||
{
|
||||
name: 'project_column',
|
||||
value: 'project_column',
|
||||
description: 'Triggered when a project column is created, updated, moved, or deleted.',
|
||||
},
|
||||
{
|
||||
name: 'project',
|
||||
value: 'project',
|
||||
description: 'Triggered when a project is created, updated, closed, reopened, or deleted.',
|
||||
},
|
||||
{
|
||||
name: 'public',
|
||||
value: 'public',
|
||||
description: 'Triggered when a private repository is open sourced.',
|
||||
},
|
||||
{
|
||||
name: 'pull_request',
|
||||
value: 'pull_request',
|
||||
description: 'Triggered when a pull request is assigned, unassigned, labeled, unlabeled, opened, edited, closed, reopened, synchronize, ready_for_review, locked, unlocked, a pull request review is requested, or a review request is removed.',
|
||||
},
|
||||
{
|
||||
name: 'pull_request_review',
|
||||
value: 'pull_request_review',
|
||||
description: 'Triggered when a pull request review is submitted into a non-pending state, the body is edited, or the review is dismissed.',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'pull_request_review_comment',
|
||||
value: 'pull_request_review_comment',
|
||||
description: 'Triggered when a comment on a pull request\'s unified diff is created, edited, or deleted (in the Files Changed tab).',
|
||||
},
|
||||
{
|
||||
name: 'push',
|
||||
value: 'push',
|
||||
description: 'Triggered on a push to a repository branch. Branch pushes and repository tag pushes also trigger webhook push events. This is the default event.',
|
||||
},
|
||||
{
|
||||
name: 'release',
|
||||
value: 'release',
|
||||
description: 'Triggered when a release is published, unpublished, created, edited, deleted, or prereleased.',
|
||||
},
|
||||
{
|
||||
name: 'repository',
|
||||
value: 'repository',
|
||||
description: 'Triggered when a repository is created, archived, unarchived, renamed, edited, transferred, made public, or made private. Organization hooks are also triggered when a repository is deleted.',
|
||||
},
|
||||
{
|
||||
name: 'repository_import',
|
||||
value: 'repository_import',
|
||||
description: 'Triggered when a successful, cancelled, or failed repository import finishes for a GitHub organization or a personal repository.',
|
||||
},
|
||||
{
|
||||
name: 'repository_vulnerability_alert',
|
||||
value: 'repository_vulnerability_alert',
|
||||
description: 'Triggered when a security alert is created, dismissed, or resolved.',
|
||||
},
|
||||
{
|
||||
name: 'security_advisory',
|
||||
value: 'security_advisory',
|
||||
description: 'Triggered when a new security advisory is published, updated, or withdrawn.',
|
||||
},
|
||||
{
|
||||
name: 'star',
|
||||
value: 'star',
|
||||
description: 'Triggered when a star is added or removed from a repository.',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
value: 'status',
|
||||
description: 'Triggered when the status of a Git commit changes.',
|
||||
},
|
||||
{
|
||||
name: 'team',
|
||||
value: 'team',
|
||||
description: 'Triggered when an organization\'s team is created,<br/>deleted, edited, added_to_repository, or removed_from_repository. Organization hooks only',
|
||||
},
|
||||
{
|
||||
name: 'team_add',
|
||||
value: 'team_add',
|
||||
description: 'Triggered when a repository is added to a team.',
|
||||
},
|
||||
{
|
||||
name: 'watch',
|
||||
value: 'watch',
|
||||
description: 'Triggered when someone stars a repository.',
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
default: [],
|
||||
description: 'The events to listen to.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-ignore (because of request)
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
if (webhookData.webhookId === undefined) {
|
||||
// No webhook id is set so no webhook can exist
|
||||
return false;
|
||||
}
|
||||
|
||||
// Webhook got created before so check if it still exists
|
||||
const owner = this.getNodeParameter('owner') as string;
|
||||
const repository = this.getNodeParameter('repository') as string;
|
||||
const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`;
|
||||
|
||||
try {
|
||||
await githubApiRequest.call(this, 'GET', endpoint, {});
|
||||
} catch (e) {
|
||||
if (e.message.includes('[404]:')) {
|
||||
// Webhook does not exist
|
||||
delete webhookData.webhookId;
|
||||
delete webhookData.webhookEvents;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some error occured
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If it did not error then the webhook exists
|
||||
return true;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
|
||||
const owner = this.getNodeParameter('owner') as string;
|
||||
const repository = this.getNodeParameter('repository') as string;
|
||||
const events = this.getNodeParameter('events', []);
|
||||
|
||||
const endpoint = `/repos/${owner}/${repository}/hooks`;
|
||||
|
||||
const body = {
|
||||
name: 'web',
|
||||
config: {
|
||||
url: webhookUrl,
|
||||
content_type: 'json',
|
||||
// secret: '...later...',
|
||||
insecure_ssl: '1', // '0' -> not allow inscure ssl | '1' -> allow insercure SSL
|
||||
},
|
||||
events,
|
||||
active: true,
|
||||
};
|
||||
|
||||
const responseData = await githubApiRequest.call(this, 'POST', endpoint, body);
|
||||
|
||||
if (responseData.id === undefined || responseData.active !== true) {
|
||||
// Required data is missing so was not successful
|
||||
throw new Error('Github webhook creation response did not contain the expected data.');
|
||||
}
|
||||
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
webhookData.webhookId = responseData.id as string;
|
||||
webhookData.webhookEvents = responseData.events as string[];
|
||||
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
if (webhookData.webhookId !== undefined) {
|
||||
const owner = this.getNodeParameter('owner') as string;
|
||||
const repository = this.getNodeParameter('repository') as string;
|
||||
const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`;
|
||||
const body = {};
|
||||
|
||||
try {
|
||||
await githubApiRequest.call(this, 'DELETE', endpoint, body);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove from the static workflow data so that it is clear
|
||||
// that no webhooks are registred anymore
|
||||
delete webhookData.webhookId;
|
||||
delete webhookData.webhookEvents;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResonseData> {
|
||||
const bodyData = this.getBodyData();
|
||||
|
||||
// Check if the webhook is only the ping from Github to confirm if it workshook_id
|
||||
if (bodyData.hook_id !== undefined && bodyData.action === undefined) {
|
||||
// Is only the ping and not an actual webhook call. So return 'OK'
|
||||
// but do not start the workflow.
|
||||
|
||||
return {
|
||||
webhookResponse: 'OK'
|
||||
};
|
||||
}
|
||||
|
||||
// Is a regular webhoook call
|
||||
|
||||
// TODO: Add headers & requestPath
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
returnData.push(
|
||||
{
|
||||
body: bodyData,
|
||||
headers: this.getHeaderData(),
|
||||
query: this.getQueryData(),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
workflowData: [
|
||||
this.helpers.returnJsonArray(returnData)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Github/github.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
220
packages/nodes-base/nodes/GoogleSheets/GoogleSheets.node.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { IGoogleAuthCredentials, GoogleSheet } from '../../src/GoogleSheet';
|
||||
|
||||
|
||||
export class GoogleSheets implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Google Sheets ',
|
||||
name: 'googleSheets',
|
||||
icon: 'file:googlesheets.png',
|
||||
group: ['input', 'output'],
|
||||
version: 1,
|
||||
description: 'Read, update and write data to Google Sheets',
|
||||
defaults: {
|
||||
name: 'Google Sheets',
|
||||
color: '#995533',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'googleApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Append',
|
||||
value: 'append',
|
||||
description: 'Appends the data to a Sheet',
|
||||
},
|
||||
{
|
||||
name: 'Read',
|
||||
value: 'read',
|
||||
description: 'Reads data from a Sheet'
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Updates rows in a sheet'
|
||||
},
|
||||
],
|
||||
default: 'read',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// All
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Sheet ID',
|
||||
name: 'sheetId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
noDataExpression: true,
|
||||
description: 'The ID of the Google Sheet.',
|
||||
},
|
||||
{
|
||||
displayName: 'Range',
|
||||
name: 'range',
|
||||
type: 'string',
|
||||
default: 'A:F',
|
||||
required: true,
|
||||
noDataExpression: true,
|
||||
description: 'The columns to read and append data to.<br />If it contains multiple sheets it can also be<br />added like this: "MySheet!A:F"',
|
||||
},
|
||||
{
|
||||
displayName: 'Key Row',
|
||||
name: 'keyRow',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
},
|
||||
default: 0,
|
||||
noDataExpression: true,
|
||||
description: 'Index of the row which contains the key. Starts with 0.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// Read & Update
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Data Start Row',
|
||||
name: 'dataStartRow',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: [
|
||||
'append'
|
||||
],
|
||||
},
|
||||
},
|
||||
noDataExpression: true,
|
||||
description: 'Index of the first row which contains<br />the actual data and not the keys. Starts with 0.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// Update
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: 'id',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'update'
|
||||
],
|
||||
},
|
||||
},
|
||||
noDataExpression: true,
|
||||
description: 'The name of the key to identify which<br />data should be updated in the sheet.',
|
||||
},
|
||||
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const spreadsheetId = this.getNodeParameter('sheetId', 0) as string;
|
||||
const credentials = this.getCredentials('googleApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const googleCredentials = {
|
||||
email: credentials.email,
|
||||
privateKey: credentials.privateKey,
|
||||
} as IGoogleAuthCredentials;
|
||||
|
||||
const sheet = new GoogleSheet(spreadsheetId, googleCredentials);
|
||||
|
||||
const range = this.getNodeParameter('range', 0) as string;
|
||||
const keyRow = this.getNodeParameter('keyRow', 0) as number;
|
||||
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
|
||||
if (operation === 'append') {
|
||||
// ----------------------------------
|
||||
// append
|
||||
// ----------------------------------
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
const setData: IDataObject[] = [];
|
||||
items.forEach((item) => {
|
||||
setData.push(item.json);
|
||||
});
|
||||
|
||||
// Convert data into array format
|
||||
const data = await sheet.appendSheetData(setData, range, keyRow);
|
||||
|
||||
// TODO: Should add this data somewhere
|
||||
// TODO: Should have something like add metadata which does not get passed through
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
} else if (operation === 'read') {
|
||||
// ----------------------------------
|
||||
// read
|
||||
// ----------------------------------
|
||||
|
||||
const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number;
|
||||
|
||||
const sheetData = await sheet.getData(range);
|
||||
|
||||
let returnData: IDataObject[];
|
||||
if (!sheetData) {
|
||||
returnData = [];
|
||||
} else {
|
||||
returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow);
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
|
||||
const keyName = this.getNodeParameter('key', 0) as string;
|
||||
const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number;
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
const setData: IDataObject[] = [];
|
||||
items.forEach((item) => {
|
||||
setData.push(item.json);
|
||||
});
|
||||
|
||||
const data = await sheet.updateSheetData(setData, keyName, range, keyRow, dataStartRow);
|
||||
|
||||
// TODO: Should add this data somewhere
|
||||
// TODO: Should have something like add metadata which does not get passed through
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/GoogleSheets/googlesheets.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
444
packages/nodes-base/nodes/HttpRequest.node.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
interface OptionData {
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface OptionDataParamters {
|
||||
[key: string]: OptionData;
|
||||
}
|
||||
|
||||
|
||||
export class HttpRequest implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'HTTP Request',
|
||||
name: 'httpRequest',
|
||||
icon: 'fa:at',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Makes a HTTP request and returns the received data',
|
||||
defaults: {
|
||||
name: 'Http Request',
|
||||
color: '#2200DD',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'httpBasicAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'basicAuth',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'httpHeaderAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'headerAuth',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Basic Auth',
|
||||
value: 'basicAuth'
|
||||
},
|
||||
{
|
||||
name: 'Header Auth',
|
||||
value: 'headerAuth'
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none'
|
||||
},
|
||||
],
|
||||
default: 'none',
|
||||
description: 'The way to authenticate.',
|
||||
},
|
||||
{
|
||||
displayName: 'Request Method',
|
||||
name: 'requestMethod',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'DELETE',
|
||||
value: 'DELETE'
|
||||
},
|
||||
{
|
||||
name: 'GET',
|
||||
value: 'GET'
|
||||
},
|
||||
{
|
||||
name: 'HEAD',
|
||||
value: 'HEAD'
|
||||
},
|
||||
{
|
||||
name: 'POST',
|
||||
value: 'POST'
|
||||
},
|
||||
{
|
||||
name: 'PUT',
|
||||
value: 'PUT'
|
||||
},
|
||||
],
|
||||
default: 'GET',
|
||||
description: 'The request method to use.',
|
||||
},
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'http://example.com/index.html',
|
||||
description: 'The URL to make the request to.',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Response Format',
|
||||
name: 'responseFormat',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'JSON',
|
||||
value: 'json'
|
||||
},
|
||||
{
|
||||
name: 'String',
|
||||
value: 'string'
|
||||
},
|
||||
{
|
||||
name: 'XML (not implemented)',
|
||||
value: 'xml'
|
||||
},
|
||||
],
|
||||
default: 'json',
|
||||
description: 'The format in which the data gets returned from the URL.',
|
||||
},
|
||||
{
|
||||
displayName: 'JSON Parameters',
|
||||
name: 'jsonParameters',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'If the query and/or body parameter should be set via the UI or raw as JSON',
|
||||
},
|
||||
|
||||
// Header Parameters
|
||||
{
|
||||
displayName: 'Headers',
|
||||
name: 'headerParametersJson',
|
||||
type: 'json',
|
||||
displayOptions: {
|
||||
show: {
|
||||
jsonParameters: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Header parameters as JSON (flat object).',
|
||||
},
|
||||
{
|
||||
displayName: 'Headers',
|
||||
name: 'headerParametersUi',
|
||||
placeholder: 'Add Header',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
jsonParameters: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The headers to send.',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'parameter',
|
||||
displayName: 'Header',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the header.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value to set for the header.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Body Parameter
|
||||
{
|
||||
displayName: 'Body Parameters',
|
||||
name: 'bodyParametersJson',
|
||||
type: 'json',
|
||||
displayOptions: {
|
||||
show: {
|
||||
jsonParameters: [
|
||||
true,
|
||||
],
|
||||
requestMethod: [
|
||||
'POST',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Body parameters as JSON.',
|
||||
},
|
||||
{
|
||||
displayName: 'Body Parameters',
|
||||
name: 'bodyParametersUi',
|
||||
placeholder: 'Add Parameter',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
jsonParameters: [
|
||||
false,
|
||||
],
|
||||
requestMethod: [
|
||||
'POST',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The body parameter to send.',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'parameter',
|
||||
displayName: 'Parameter',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the parameter.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value of the parameter.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Query Parameter
|
||||
{
|
||||
displayName: 'Query Parameters',
|
||||
name: 'queryParametersJson',
|
||||
type: 'json',
|
||||
displayOptions: {
|
||||
show: {
|
||||
jsonParameters: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Query parameters as JSON (flat object).',
|
||||
},
|
||||
{
|
||||
displayName: 'Query Parameters',
|
||||
name: 'queryParametersUi',
|
||||
placeholder: 'Add Parameter',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
jsonParameters: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The query parameter to send.',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'parameter',
|
||||
displayName: 'Parameter',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the parameter.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value of the parameter.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push({json: {}});
|
||||
}
|
||||
|
||||
// TODO: Should have a setting which makes clear that this parameter can not change for each item
|
||||
const requestMethod = this.getNodeParameter('requestMethod', 0) as string;
|
||||
const parametersAreJson = this.getNodeParameter('jsonParameters', 0) as boolean;
|
||||
|
||||
const httpBasicAuth = this.getCredentials('httpBasicAuth');
|
||||
const httpHeaderAuth = this.getCredentials('httpHeaderAuth');
|
||||
|
||||
let item: INodeExecutionData;
|
||||
let url: string, responseFormat: string;
|
||||
let requestOptions: OptionsWithUri;
|
||||
let setUiParameter: IDataObject;
|
||||
|
||||
const uiParameters: IDataObject = {
|
||||
bodyParametersUi: 'body',
|
||||
headerParametersUi: 'headers',
|
||||
queryParametersUi: 'qs',
|
||||
};
|
||||
|
||||
const jsonParameters: OptionDataParamters = {
|
||||
bodyParametersJson: {
|
||||
name: 'body',
|
||||
displayName: 'Body Parameters',
|
||||
},
|
||||
headerParametersJson: {
|
||||
name: 'headers',
|
||||
displayName: 'Headers',
|
||||
},
|
||||
queryParametersJson: {
|
||||
name: 'qs',
|
||||
displayName: 'Query Paramters',
|
||||
},
|
||||
};
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
item = items[itemIndex];
|
||||
|
||||
url = this.getNodeParameter('url', itemIndex) as string;
|
||||
responseFormat = this.getNodeParameter('responseFormat', itemIndex) as string;
|
||||
|
||||
requestOptions = {
|
||||
headers: {},
|
||||
method: requestMethod,
|
||||
uri: url,
|
||||
};
|
||||
|
||||
if (parametersAreJson === true) {
|
||||
// Parameters are defined as JSON
|
||||
let optionData: OptionData;
|
||||
for (const parameterName of Object.keys(jsonParameters)) {
|
||||
optionData = jsonParameters[parameterName] as OptionData;
|
||||
const tempValue = this.getNodeParameter(parameterName, itemIndex, {}) as string | object;
|
||||
if (tempValue === '') {
|
||||
// Paramter is empty so skip it
|
||||
continue;
|
||||
}
|
||||
// @ts-ignore
|
||||
requestOptions[optionData.name] = tempValue;
|
||||
|
||||
// @ts-ignore
|
||||
if (typeof requestOptions[optionData.name] !== 'object') {
|
||||
// If it is not an object it must be JSON so parse it
|
||||
try {
|
||||
// @ts-ignore
|
||||
requestOptions[optionData.name] = JSON.parse(requestOptions[optionData.name]);
|
||||
} catch (e) {
|
||||
throw new Error(`The data in "${optionData.displayName}" is no valid JSON.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Paramters are defined in UI
|
||||
let optionName: string;
|
||||
for (const parameterName of Object.keys(uiParameters)) {
|
||||
setUiParameter = this.getNodeParameter(parameterName, itemIndex, {}) as IDataObject;
|
||||
optionName = uiParameters[parameterName] as string;
|
||||
if (setUiParameter.parameter !== undefined) {
|
||||
// @ts-ignore
|
||||
requestOptions[optionName] = {};
|
||||
for (const parameterData of setUiParameter!.parameter as IDataObject[]) {
|
||||
// @ts-ignore
|
||||
requestOptions[optionName][parameterData!.name as string] = parameterData!.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
requestOptions.json = true;
|
||||
|
||||
// Add credentials if any are set
|
||||
if (httpBasicAuth !== undefined) {
|
||||
requestOptions.auth = {
|
||||
user: httpBasicAuth.user as string,
|
||||
pass: httpBasicAuth.password as string,
|
||||
};
|
||||
}
|
||||
if (httpHeaderAuth !== undefined) {
|
||||
requestOptions.headers![httpHeaderAuth.name as string] = httpHeaderAuth.value;
|
||||
}
|
||||
|
||||
// Now that the options are all set make the actual http request
|
||||
const response = await this.helpers.request(requestOptions);
|
||||
|
||||
if (responseFormat === 'json') {
|
||||
item.json = response;
|
||||
} else if (responseFormat === 'xml') {
|
||||
// TODO: Not implemented yet
|
||||
} else {
|
||||
item.json.reponse = response;
|
||||
}
|
||||
}
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
302
packages/nodes-base/nodes/If.node.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class If implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'IF',
|
||||
name: 'if',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Splits a stream depending on defined compare operations.',
|
||||
defaults: {
|
||||
name: 'IF',
|
||||
color: '#408000',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main', 'main'],
|
||||
outputNames: ['true', 'false'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Conditions',
|
||||
name: 'conditions',
|
||||
placeholder: 'Add Condition',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
description: 'The type of values to compare.',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'boolean',
|
||||
displayName: 'Boolean',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'The value to compare with the second one.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Equal',
|
||||
value: 'equal'
|
||||
},
|
||||
{
|
||||
name: 'Not Equal',
|
||||
value: 'notEqual'
|
||||
},
|
||||
],
|
||||
default: 'equal',
|
||||
description: 'Operation to decide where the the data should be mapped to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'The value to compare with the first one.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The value to compare with the second one.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Smaller',
|
||||
value: 'smaller'
|
||||
},
|
||||
{
|
||||
name: 'Equal',
|
||||
value: 'equal'
|
||||
},
|
||||
{
|
||||
name: 'Not Equal',
|
||||
value: 'notEqual'
|
||||
},
|
||||
{
|
||||
name: 'Larger',
|
||||
value: 'larger'
|
||||
},
|
||||
],
|
||||
default: 'smaller',
|
||||
description: 'Operation to decide where the the data should be mapped to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The value to compare with the first one.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The value to compare with the second one.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Contains',
|
||||
value: 'contains'
|
||||
},
|
||||
{
|
||||
name: 'Equal',
|
||||
value: 'equal'
|
||||
},
|
||||
{
|
||||
name: 'Not Contains',
|
||||
value: 'notContains'
|
||||
},
|
||||
{
|
||||
name: 'Not Equal',
|
||||
value: 'notEqual'
|
||||
},
|
||||
{
|
||||
name: 'Regex',
|
||||
value: 'regex'
|
||||
},
|
||||
],
|
||||
default: 'equal',
|
||||
description: 'Operation to decide where the the data should be mapped to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: [
|
||||
'regex',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'The value to compare with the first one.',
|
||||
},
|
||||
{
|
||||
displayName: 'Regex',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'regex',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: '/text/i',
|
||||
description: 'The regex which has to match.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Combine',
|
||||
name: 'combineOperation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'ALL',
|
||||
description: 'Only if all conditions are meet it goes into "true" branch.',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
name: 'ANY',
|
||||
description: 'If any of the conditions is meet it goes into "true" branch.',
|
||||
value: 'any'
|
||||
},
|
||||
],
|
||||
default: 'all',
|
||||
description: 'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const returnDataTrue: INodeExecutionData[] = [];
|
||||
const returnDataFalse: INodeExecutionData[] = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
let item: INodeExecutionData;
|
||||
let combineOperation: string;
|
||||
|
||||
// The compare operations
|
||||
const compareOperationFunctions: {
|
||||
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
|
||||
} = {
|
||||
contains: (value1: NodeParameterValue, value2: NodeParameterValue) => value1.toString().includes(value2.toString()),
|
||||
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !value1.toString().includes(value2.toString()),
|
||||
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
|
||||
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
|
||||
larger: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 > value2,
|
||||
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 < value2,
|
||||
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
||||
const regexMatch = value2.toString().match(new RegExp('^/(.*?)/([gimy]*)$'));
|
||||
|
||||
let regex: RegExp;
|
||||
if (!regexMatch) {
|
||||
regex = new RegExp(value2.toString());
|
||||
} else if (regexMatch.length === 1) {
|
||||
regex = new RegExp(regexMatch[1]);
|
||||
} else {
|
||||
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
||||
}
|
||||
|
||||
return !!value1.toString().match(regex);
|
||||
},
|
||||
};
|
||||
|
||||
// The different dataTypes to check the values in
|
||||
const dataTypes = [
|
||||
'boolean',
|
||||
'number',
|
||||
'string',
|
||||
];
|
||||
|
||||
// Itterate over all items to check which ones should be output as via output "true" and
|
||||
// which ones via output "false"
|
||||
let dataType: string;
|
||||
let compareOperationResult: boolean;
|
||||
itemLoop:
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
item = items[itemIndex];
|
||||
|
||||
let compareData: INodeParameters;
|
||||
|
||||
combineOperation = this.getNodeParameter('combineOperation', itemIndex) as string;
|
||||
|
||||
// Check all the values of the different dataTypes
|
||||
for (dataType of dataTypes) {
|
||||
// Check all the values of the current dataType
|
||||
for (compareData of this.getNodeParameter(`conditions.${dataType}`, itemIndex, []) as INodeParameters[]) {
|
||||
// Check if the values passes
|
||||
compareOperationResult = compareOperationFunctions[compareData.operation as string](compareData.value1 as NodeParameterValue, compareData.value2 as NodeParameterValue);
|
||||
|
||||
if (compareOperationResult === true && combineOperation === 'any') {
|
||||
// If it passes and the operation is "any" we do not have to check any
|
||||
// other ones as it should pass anyway. So go on with the next item.
|
||||
returnDataTrue.push(item);
|
||||
continue itemLoop;
|
||||
} else if (compareOperationResult === false && combineOperation === 'all') {
|
||||
// If it fails and the operation is "all" we do not have to check any
|
||||
// other ones as it should be not pass anyway. So go on with the next item.
|
||||
returnDataFalse.push(item);
|
||||
continue itemLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (combineOperation === 'all') {
|
||||
// If the operation is "all" it means the item did match all conditions
|
||||
// so it passes.
|
||||
returnDataTrue.push(item);
|
||||
} else {
|
||||
// If the operation is "any" it means the the item did not match any condition.
|
||||
returnDataFalse.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [returnDataTrue, returnDataFalse];
|
||||
}
|
||||
}
|
||||
96
packages/nodes-base/nodes/Interval.node.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ITriggerFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ITriggerResponse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class Interval implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Interval',
|
||||
name: 'interval',
|
||||
icon: 'fa:hourglass',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Triggers the workflow in a given interval',
|
||||
defaults: {
|
||||
name: 'Interval',
|
||||
color: '#00FF00',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Interval',
|
||||
name: 'interval',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description: 'Interval value.',
|
||||
},
|
||||
{
|
||||
displayName: 'Unit',
|
||||
name: 'unit',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Seconds',
|
||||
value: 'seconds'
|
||||
},
|
||||
{
|
||||
name: 'Minutes',
|
||||
value: 'minutes'
|
||||
},
|
||||
{
|
||||
name: 'Hours',
|
||||
value: 'hours'
|
||||
},
|
||||
],
|
||||
default: 'seconds',
|
||||
description: 'Unit of the interval value.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
|
||||
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||
const interval = this.getNodeParameter('interval') as number;
|
||||
const unit = this.getNodeParameter('unit') as string;
|
||||
|
||||
if (interval <= 0) {
|
||||
throw new Error('The interval has to be set to at least 1 or higher!');
|
||||
}
|
||||
|
||||
let intervalValue = interval;
|
||||
if (unit === 'minutes') {
|
||||
intervalValue *= 60;
|
||||
}
|
||||
if (unit === 'hours') {
|
||||
intervalValue *= 60 * 60;
|
||||
}
|
||||
|
||||
const executeTrigger = () => {
|
||||
this.emit([this.helpers.returnJsonArray([{}])]);
|
||||
};
|
||||
|
||||
const intervalObj = setInterval(executeTrigger, intervalValue * 1000);
|
||||
|
||||
async function closeFunction() {
|
||||
clearInterval(intervalObj);
|
||||
}
|
||||
|
||||
async function manualTriggerFunction() {
|
||||
executeTrigger();
|
||||
}
|
||||
|
||||
return {
|
||||
closeFunction,
|
||||
manualTriggerFunction,
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
338
packages/nodes-base/nodes/LinkFish/LinkFish.node.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { IExecuteSingleFunctions } from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class LinkFish implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'link.fish Scrape',
|
||||
name: 'linkFish',
|
||||
icon: 'file:linkfish.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Scrape data from an URL',
|
||||
defaults: {
|
||||
name: 'link.fish Scrape',
|
||||
color: '#33AA22',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'linkFishApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'All Data',
|
||||
value: 'data',
|
||||
description: 'Get all found data',
|
||||
},
|
||||
{
|
||||
name: 'Apps',
|
||||
value: 'apps',
|
||||
description: 'Get mobile app information',
|
||||
},
|
||||
{
|
||||
name: 'Social Media',
|
||||
value: 'socialMedia',
|
||||
description: 'Get social-media profiles',
|
||||
},
|
||||
{
|
||||
name: 'Screenshot',
|
||||
value: 'screenshot',
|
||||
description: 'Get screenshot',
|
||||
},
|
||||
],
|
||||
default: 'data',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// All
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'URL of which the data should be extracted.',
|
||||
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// data
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Item Format',
|
||||
name: 'itemFormat',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Normal',
|
||||
value: 'normal'
|
||||
},
|
||||
{
|
||||
name: 'Flat',
|
||||
value: 'flat'
|
||||
},
|
||||
],
|
||||
default: 'flat',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'data'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'If data should be returned in the "normal"<br />format or in the "flat" one which has all<br />items on one level.',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Types',
|
||||
name: 'simplifySpecialTypes',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'data'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'If special types like "ParameterValue"<br />should be simplified to a key -> value pair.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// screenshot
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Normal',
|
||||
value: 'normal',
|
||||
description: 'Creates a screenshot in 16:9 format.',
|
||||
},
|
||||
{
|
||||
name: 'Full',
|
||||
value: 'full',
|
||||
description: 'Creates a full page screenshot.',
|
||||
},
|
||||
],
|
||||
default: 'normal',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'screenshot'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The file format the screenshot should be returned as.',
|
||||
},
|
||||
{
|
||||
displayName: 'File Format',
|
||||
name: 'fileFormat',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'JPG',
|
||||
value: 'jpg',
|
||||
},
|
||||
{
|
||||
name: 'PNG',
|
||||
value: 'png',
|
||||
},
|
||||
],
|
||||
default: 'jpg',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'screenshot'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The file format the screenshot should be returned as.',
|
||||
},
|
||||
{
|
||||
displayName: 'Width',
|
||||
name: 'width',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 50,
|
||||
maxValue: 1280,
|
||||
},
|
||||
default: 640,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'screenshot'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The width of the screenshot in pixel.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'screenshot',
|
||||
],
|
||||
},
|
||||
|
||||
},
|
||||
placeholder: '',
|
||||
description: 'Name of the binary property in which to save<br />the binary data of the screenshot.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// Multiple ones
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Browser Render',
|
||||
name: 'browserRender',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: [
|
||||
'screenshot'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Renders the website in a browser<br />(charges 5 instead of 1 credit!)',
|
||||
},
|
||||
{
|
||||
displayName: 'Return URLs',
|
||||
name: 'returnUrls',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: [
|
||||
'data',
|
||||
'screenshot',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Returns app URLs instead of the identifiers.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const url = this.getNodeParameter('url') as string;
|
||||
|
||||
const credentials = this.getCredentials('linkFishApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
let requestMethod = 'GET';
|
||||
|
||||
// For Post
|
||||
const body: IDataObject = {
|
||||
url,
|
||||
};
|
||||
// For Query string
|
||||
const qs: IDataObject = {};
|
||||
|
||||
let endpoint: string;
|
||||
let encoding: string | null = 'utf8';
|
||||
|
||||
const operation = this.getNodeParameter('operation') as string;
|
||||
|
||||
if (operation === 'data') {
|
||||
requestMethod = 'POST';
|
||||
|
||||
body.browser_render = this.getNodeParameter('browserRender') as boolean;
|
||||
body.item_format = this.getNodeParameter('itemFormat') as string;
|
||||
body.simplify_special_types = this.getNodeParameter('simplifySpecialTypes') as boolean;
|
||||
|
||||
endpoint = 'data';
|
||||
} else if (operation === 'apps') {
|
||||
requestMethod = 'GET';
|
||||
|
||||
qs.browser_render = this.getNodeParameter('browserRender') as boolean;
|
||||
qs.return_urls = this.getNodeParameter('returnUrls') as boolean;
|
||||
|
||||
endpoint = 'apps';
|
||||
} else if (operation === 'socialMedia') {
|
||||
requestMethod = 'GET';
|
||||
|
||||
qs.browser_render = this.getNodeParameter('browserRender') as boolean;
|
||||
qs.return_urls = this.getNodeParameter('returnUrls') as boolean;
|
||||
|
||||
endpoint = 'social-media';
|
||||
} else if (operation === 'screenshot') {
|
||||
requestMethod = 'GET';
|
||||
encoding = null;
|
||||
|
||||
qs.file_format = this.getNodeParameter('fileFormat') as string;
|
||||
qs.type = this.getNodeParameter('type') as string;
|
||||
qs.width = this.getNodeParameter('width') as number;
|
||||
|
||||
endpoint = 'browser-screenshot';
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not supported!`);
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: requestMethod,
|
||||
body,
|
||||
qs,
|
||||
uri: `https://api.link.fish/Urls/${endpoint}`,
|
||||
auth: {
|
||||
user: credentials.email as string,
|
||||
pass: credentials.apiKey as string,
|
||||
},
|
||||
encoding,
|
||||
json: true
|
||||
};
|
||||
|
||||
const responseData = await this.helpers.request(options);
|
||||
|
||||
if (operation === 'screenshot') {
|
||||
const item = this.getInputData();
|
||||
|
||||
// Add the returned data to the item as binary property
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName') as string;
|
||||
if (item.binary === undefined) {
|
||||
item.binary = {};
|
||||
}
|
||||
|
||||
let fileExtension = 'png';
|
||||
let mimeType = 'image/png';
|
||||
if (qs.file_format === 'jpg') {
|
||||
fileExtension = 'jpg';
|
||||
mimeType = 'image/jpeg';
|
||||
}
|
||||
|
||||
item.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(responseData, `screenshot.${fileExtension}`, mimeType);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
json: responseData,
|
||||
};
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/LinkFish/linkfish.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
184
packages/nodes-base/nodes/Mailgun.node.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteSingleFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class Mailgun implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Mailgun',
|
||||
name: 'mailgun',
|
||||
icon: 'fa:envelope',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
description: 'Sends an Email via Mailgun',
|
||||
defaults: {
|
||||
name: 'Mailgun',
|
||||
color: '#44DD22',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'mailgunApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'From Email',
|
||||
name: 'fromEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'Admin <admin@example.com>',
|
||||
description: 'Email address of the sender optional with name.',
|
||||
},
|
||||
{
|
||||
displayName: 'To Email',
|
||||
name: 'toEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'info@example.com',
|
||||
description: 'Email address of the recipient. Multiple ones can be separated by comma.',
|
||||
},
|
||||
{
|
||||
displayName: 'Cc Email',
|
||||
name: 'ccEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
description: 'Cc Email address of the recipient. Multiple ones can be separated by comma.',
|
||||
},
|
||||
{
|
||||
displayName: 'Bcc Email',
|
||||
name: 'bccEmail',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
description: 'Bcc Email address of the recipient. Multiple ones can be separated by comma.',
|
||||
},
|
||||
{
|
||||
displayName: 'Subject',
|
||||
name: 'subject',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'My subject line',
|
||||
description: 'Subject line of the email.',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
rows: 5,
|
||||
},
|
||||
default: '',
|
||||
description: 'Plain text message of email.',
|
||||
},
|
||||
{
|
||||
displayName: 'HTML',
|
||||
name: 'html',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
default: '',
|
||||
description: 'HTML text message of email.',
|
||||
},
|
||||
{
|
||||
displayName: 'Attachments',
|
||||
name: 'attachments',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the binary properties which contain<br />data which should be added to email as attachment.<br />Multiple ones can be comma separated.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const item = this.getInputData();
|
||||
|
||||
const fromEmail = this.getNodeParameter('fromEmail') as string;
|
||||
const toEmail = this.getNodeParameter('toEmail') as string;
|
||||
const ccEmail = this.getNodeParameter('ccEmail') as string;
|
||||
const bccEmail = this.getNodeParameter('bccEmail') as string;
|
||||
const subject = this.getNodeParameter('subject') as string;
|
||||
const text = this.getNodeParameter('text') as string;
|
||||
const html = this.getNodeParameter('html') as string;
|
||||
const attachmentPropertyString = this.getNodeParameter('attachments') as string;
|
||||
|
||||
const credentials = this.getCredentials('mailgunApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const formData: IDataObject = {
|
||||
from: fromEmail,
|
||||
to: toEmail,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
};
|
||||
|
||||
if (ccEmail.length !== 0) {
|
||||
formData.cc = ccEmail;
|
||||
}
|
||||
if (bccEmail.length !== 0) {
|
||||
formData.bcc = bccEmail;
|
||||
}
|
||||
|
||||
if (attachmentPropertyString && item.binary) {
|
||||
|
||||
const attachments = [];
|
||||
const attachmentProperties: string[] = attachmentPropertyString.split(',').map((propertyName) => {
|
||||
return propertyName.trim();
|
||||
});
|
||||
|
||||
for (const propertyName of attachmentProperties) {
|
||||
if (!item.binary.hasOwnProperty(propertyName)) {
|
||||
continue;
|
||||
}
|
||||
attachments.push({
|
||||
value: Buffer.from(item.binary[propertyName].data, BINARY_ENCODING),
|
||||
options: {
|
||||
filename: item.binary[propertyName].fileName || 'unknown',
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (attachments.length) {
|
||||
// @ts-ignore
|
||||
formData.attachment = attachments;
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
formData,
|
||||
uri: `https://${credentials.apiDomain}/v3/${credentials.emailDomain}/messages`,
|
||||
auth: {
|
||||
user: 'api',
|
||||
pass: credentials.apiKey as string,
|
||||
},
|
||||
json: true,
|
||||
};
|
||||
|
||||
const responseData = await this.helpers.request(options);
|
||||
|
||||
return {
|
||||
json: responseData,
|
||||
};
|
||||
}
|
||||
}
|
||||
154
packages/nodes-base/nodes/Merge.node.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
GenericValue,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class Merge implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Merge',
|
||||
name: 'merge',
|
||||
icon: 'fa:clone',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Merges data from multiple streams',
|
||||
defaults: {
|
||||
name: 'Merge',
|
||||
color: '#00cc22',
|
||||
},
|
||||
inputs: ['main', 'main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Mode',
|
||||
name: 'mode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Append',
|
||||
value: 'append'
|
||||
},
|
||||
{
|
||||
name: 'Merge',
|
||||
value: 'merge'
|
||||
},
|
||||
],
|
||||
default: 'append',
|
||||
description: 'How data should be merged. If it should simply<br />be appended or merged depending on a property.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Input 1',
|
||||
name: 'propertyName1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: [
|
||||
'merge'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Name of property which decides which items to merge of input 1.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Input 2',
|
||||
name: 'propertyName2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: [
|
||||
'merge'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Name of property which decides which items to merge of input 2.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
// const itemsInput2 = this.getInputData(1);
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const mode = this.getNodeParameter('mode', 0) as string;
|
||||
|
||||
if (mode === 'append') {
|
||||
// Simply appends the data
|
||||
for (let i = 0; i < 2; i++) {
|
||||
returnData.push.apply(returnData, this.getInputData(i));
|
||||
}
|
||||
} else if (mode === 'merge') {
|
||||
// Merges data by key
|
||||
const dataInput1 = this.getInputData(0);
|
||||
if (!dataInput1) {
|
||||
// If it has no input data from first input return nothing
|
||||
return [returnData];
|
||||
}
|
||||
|
||||
const propertyName1 = this.getNodeParameter('propertyName1', 0) as string;
|
||||
const propertyName2 = this.getNodeParameter('propertyName2', 0) as string;
|
||||
|
||||
const dataInput2 = this.getInputData(1);
|
||||
if (!dataInput2 || !propertyName1 || !propertyName2) {
|
||||
// If the second input does not have any data or the property names are not defined
|
||||
// simply return the data from the first input
|
||||
return [dataInput1];
|
||||
}
|
||||
|
||||
// Get the data to copy
|
||||
const copyData: {
|
||||
[key: string]: INodeExecutionData;
|
||||
} = {};
|
||||
let entry: INodeExecutionData;
|
||||
for (entry of dataInput2) {
|
||||
if (!entry.json || !entry.json.hasOwnProperty(propertyName2)) {
|
||||
// Entry does not have the property so skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
copyData[entry.json[propertyName2] as string] = entry;
|
||||
}
|
||||
|
||||
// Copy data on entries
|
||||
let referenceValue: GenericValue;
|
||||
let key: string;
|
||||
for (entry of dataInput1) {
|
||||
|
||||
if (!entry.json || !entry.json.hasOwnProperty(propertyName1)) {
|
||||
// Entry does not have the property so skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
referenceValue = entry.json[propertyName1];
|
||||
|
||||
if (!['string', 'number'].includes(typeof referenceValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof referenceValue === 'number') {
|
||||
referenceValue = referenceValue.toString();
|
||||
}
|
||||
|
||||
if (copyData.hasOwnProperty(referenceValue as string)) {
|
||||
if (['null', 'undefined'].includes(typeof referenceValue)) {
|
||||
continue;
|
||||
}
|
||||
for (key of Object.keys(copyData[referenceValue as string].json)) {
|
||||
// TODO: Currently only copies json data and no binary one
|
||||
entry.json[key] = copyData[referenceValue as string].json[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [dataInput1];
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
534
packages/nodes-base/nodes/NextCloud/NextCloud.node.ts
Normal file
@@ -0,0 +1,534 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { parseString } from 'xml2js';
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
|
||||
export class NextCloud implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'NextCloud',
|
||||
name: 'nextCloud',
|
||||
icon: 'file:nextcloud.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Access data on NextCloud',
|
||||
defaults: {
|
||||
name: 'NextCloud',
|
||||
color: '#22BB44',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'nextCloudApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Copy',
|
||||
value: 'copy',
|
||||
description: 'Copy a file or folder',
|
||||
},
|
||||
{
|
||||
name: 'Create Folder',
|
||||
value: 'createFolder',
|
||||
description: 'Creates a folder',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Deletes a file or folder',
|
||||
},
|
||||
{
|
||||
name: 'Download File',
|
||||
value: 'downloadFile',
|
||||
description: 'Downloads a file',
|
||||
},
|
||||
{
|
||||
name: 'Get Folder Content',
|
||||
value: 'listFolderContent',
|
||||
description: 'Returns the files and folder in a given folder',
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
value: 'move',
|
||||
description: 'Moves a file or folder',
|
||||
},
|
||||
{
|
||||
name: 'Upload File',
|
||||
value: 'uploadFile',
|
||||
description: 'Uploads a file into a folder',
|
||||
},
|
||||
],
|
||||
default: 'uploadFile',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// copy
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'From Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'copy'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/original.txt',
|
||||
description: 'The path of file or folder to copy.',
|
||||
},
|
||||
{
|
||||
displayName: 'To Path',
|
||||
name: 'toPath',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'copy'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/copy.txt',
|
||||
description: 'The destination path of file or folder.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// createFolder
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Folder',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'createFolder'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'invoices/2019',
|
||||
description: 'The folder to create. The parent folder has to exist.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Delete Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'delete'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'invoices/2019/invoice_1.pdf',
|
||||
description: 'The path to delete. Can be a single file or a whole folder.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// downloadFile
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'File Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'downloadFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'invoices/2019/invoice_1.pdf',
|
||||
description: 'The file path of the file to download. Has to contain the full path.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'downloadFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Name of the binary property to which to<br />write the data of the read file.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// listFolderContent
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Folder Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'listFolderContent'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'invoices/2019/',
|
||||
description: 'The path of which to list the content.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// move
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'From Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'move'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/old_name.txt',
|
||||
description: 'The path of file or folder to move.',
|
||||
},
|
||||
{
|
||||
displayName: 'To Path',
|
||||
name: 'toPath',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'move'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: '/invoices/new_name.txt',
|
||||
description: 'The new path of file or folder.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// uploadFile
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'File Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'uploadFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'invoices/2019/invoice_1.pdf',
|
||||
description: 'The file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Data',
|
||||
name: 'binaryDataUpload',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'uploadFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
displayName: 'File Content',
|
||||
name: 'fileContent',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'uploadFile'
|
||||
],
|
||||
binaryDataUpload: [
|
||||
false
|
||||
],
|
||||
},
|
||||
|
||||
},
|
||||
placeholder: '',
|
||||
description: 'The text content of the file to upload.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'uploadFile'
|
||||
],
|
||||
binaryDataUpload: [
|
||||
true
|
||||
],
|
||||
},
|
||||
|
||||
},
|
||||
placeholder: '',
|
||||
description: 'Name of the binary property which contains<br />the data for the file to be uploaded.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const credentials = this.getCredentials('nextCloudApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
let endpoint = '';
|
||||
let requestMethod = '';
|
||||
|
||||
let body: string | Buffer = '';
|
||||
const headers: IDataObject = {};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
if (operation === 'copy') {
|
||||
// ----------------------------------
|
||||
// copy
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'COPY';
|
||||
endpoint = this.getNodeParameter('path', i) as string;
|
||||
const toPath = this.getNodeParameter('toPath', i) as string;
|
||||
headers.Destination = `${credentials.webDavUrl}/${encodeURI(toPath)}`;
|
||||
|
||||
} else if (operation === 'createFolder') {
|
||||
// ----------------------------------
|
||||
// createFolder
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'MKCOL';
|
||||
endpoint = this.getNodeParameter('path', i) as string;
|
||||
|
||||
} else if (operation === 'delete') {
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'DELETE';
|
||||
endpoint = this.getNodeParameter('path', i) as string;
|
||||
|
||||
} else if (operation === 'downloadFile') {
|
||||
// ----------------------------------
|
||||
// downloadFile
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'GET';
|
||||
endpoint = this.getNodeParameter('path', i) as string;
|
||||
|
||||
} else if (operation === 'listFolderContent') {
|
||||
// ----------------------------------
|
||||
// listFolderContent
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'PROPFIND';
|
||||
endpoint = this.getNodeParameter('path', i) as string;
|
||||
|
||||
} else if (operation === 'move') {
|
||||
// ----------------------------------
|
||||
// move
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'MOVE';
|
||||
endpoint = this.getNodeParameter('path', i) as string;
|
||||
const toPath = this.getNodeParameter('toPath', i) as string;
|
||||
headers.Destination = `${credentials.webDavUrl}/${encodeURI(toPath)}`;
|
||||
|
||||
} else if (operation === 'uploadFile') {
|
||||
// ----------------------------------
|
||||
// uploadFile
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'PUT';
|
||||
endpoint = this.getNodeParameter('path', i) as string;
|
||||
|
||||
if (this.getNodeParameter('binaryDataUpload', i) === true) {
|
||||
// Is binary file to upload
|
||||
const item = items[i];
|
||||
|
||||
if (item.binary === undefined) {
|
||||
throw new Error('No binary data exists on item!');
|
||||
}
|
||||
|
||||
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||
|
||||
|
||||
if (item.binary[propertyNameUpload] === undefined) {
|
||||
throw new Error(`No binary data property "${propertyNameUpload}" does not exists on item!`);
|
||||
}
|
||||
|
||||
body = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING);
|
||||
} else {
|
||||
// Is text file
|
||||
body = this.getNodeParameter('fileContent', i) as string;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not known!`);
|
||||
}
|
||||
|
||||
// Make sure that the webdav URL does never have a trailing slash because
|
||||
// one gets added always automatically
|
||||
let webDavUrl = credentials.webDavUrl as string;
|
||||
if (webDavUrl.slice(-1) === '/') {
|
||||
webDavUrl = webDavUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
auth: {
|
||||
user: credentials.user as string,
|
||||
pass: credentials.password as string,
|
||||
},
|
||||
headers,
|
||||
method: requestMethod,
|
||||
body,
|
||||
qs: {},
|
||||
uri: `${credentials.webDavUrl}/${encodeURI(endpoint)}`,
|
||||
json: false,
|
||||
};
|
||||
|
||||
if (operation === 'downloadFile') {
|
||||
// Return the data as a buffer
|
||||
options.encoding = null;
|
||||
}
|
||||
|
||||
const responseData = await this.helpers.request(options);
|
||||
|
||||
if (operation === 'downloadFile') {
|
||||
// TODO: Has to check if it already exists and only add if not
|
||||
if (items[i].binary === undefined) {
|
||||
items[i].binary = {};
|
||||
}
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||
|
||||
items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(responseData, endpoint);
|
||||
} else if (operation === 'listFolderContent') {
|
||||
|
||||
const jsonResponseData: IDataObject = await new Promise((resolve, reject) => {
|
||||
parseString(responseData, { explicitArray: false }, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data as IDataObject);
|
||||
});
|
||||
});
|
||||
|
||||
const propNames: { [key: string]: string } = {
|
||||
'd:getlastmodified': 'lastModified',
|
||||
'd:getcontentlength': 'contentLength',
|
||||
'd:getcontenttype': 'contentType',
|
||||
};
|
||||
|
||||
if (jsonResponseData['d:multistatus'] !== undefined &&
|
||||
jsonResponseData['d:multistatus'] !== null &&
|
||||
(jsonResponseData['d:multistatus'] as IDataObject)['d:response'] !== undefined &&
|
||||
(jsonResponseData['d:multistatus'] as IDataObject)['d:response'] !== null) {
|
||||
let skippedFirst = false;
|
||||
|
||||
// @ts-ignore
|
||||
for (const item of jsonResponseData['d:multistatus']['d:response']) {
|
||||
if (skippedFirst === false) {
|
||||
skippedFirst = true;
|
||||
continue;
|
||||
}
|
||||
const newItem: IDataObject = {};
|
||||
|
||||
newItem.path = item['d:href'].slice(19);
|
||||
|
||||
const props = item['d:propstat'][0]['d:prop'];
|
||||
|
||||
// Get the props and save them under a proper name
|
||||
for (const propName of Object.keys(propNames)) {
|
||||
if (props[propName] !== undefined) {
|
||||
newItem[propNames[propName]] = props[propName];
|
||||
}
|
||||
}
|
||||
|
||||
if (props['d:resourcetype'] === '') {
|
||||
newItem.type = 'file';
|
||||
} else {
|
||||
newItem.type = 'folder';
|
||||
}
|
||||
newItem.eTag = props['d:getetag'].slice(1, -1);
|
||||
|
||||
returnData.push(newItem as IDataObject);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'downloadFile') {
|
||||
// For file downloads the files get attached to the existing items
|
||||
return this.prepareOutputData(items);
|
||||
} else {
|
||||
// For all other ones does the output get replaced
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/NextCloud/nextcloud.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
32
packages/nodes-base/nodes/NoOp.node.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class NoOp implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'No Operation',
|
||||
name: 'noOp',
|
||||
icon: 'fa:pen',
|
||||
group: ['organization'],
|
||||
version: 1,
|
||||
description: 'No Operation',
|
||||
defaults: {
|
||||
name: 'NoOp',
|
||||
color: '#b0b0b0',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
],
|
||||
};
|
||||
|
||||
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
265
packages/nodes-base/nodes/OpenWeatherMap.node.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
export class OpenWeatherMap implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'OpenWeatherMap',
|
||||
name: 'openWeatherMap',
|
||||
icon: 'fa:sun',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Gets current and future weather information.',
|
||||
defaults: {
|
||||
name: 'OpenWeatherMap',
|
||||
color: '#554455',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'openWeatherMapApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Current Weather',
|
||||
value: 'currentWeather',
|
||||
description: 'Returns the current weather data',
|
||||
},
|
||||
{
|
||||
name: '5 day Forecast',
|
||||
value: '5DayForecast',
|
||||
description: 'Returns the weather data for the next 5 days',
|
||||
},
|
||||
],
|
||||
default: 'currentWeather',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'format',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Imperial',
|
||||
value: 'imperial',
|
||||
description: 'Fahrenheit | miles/hour',
|
||||
},
|
||||
{
|
||||
name: 'Metric',
|
||||
value: 'metric',
|
||||
description: 'Celsius | meter/sec',
|
||||
},
|
||||
{
|
||||
name: 'Scientific',
|
||||
value: 'standard',
|
||||
description: 'Kelvin | meter/sec',
|
||||
},
|
||||
],
|
||||
default: 'metric',
|
||||
description: 'The format in which format the data should be returned.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// Location Information
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Location Selection',
|
||||
name: 'locationSelection',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'City Name',
|
||||
value: 'cityName',
|
||||
},
|
||||
{
|
||||
name: 'City ID',
|
||||
value: 'cityId',
|
||||
},
|
||||
{
|
||||
name: 'Coordinates',
|
||||
value: 'coordinates',
|
||||
},
|
||||
{
|
||||
name: 'Zip Code',
|
||||
value: 'zipCode',
|
||||
},
|
||||
],
|
||||
default: 'cityName',
|
||||
description: 'How to define the location for which to return the weather.',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'cityName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'berlin,de',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
locationSelection: [
|
||||
'cityName',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The name of the city to return the weather of.',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'City ID',
|
||||
name: 'cityId',
|
||||
type: 'number',
|
||||
default: 160001123,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
locationSelection: [
|
||||
'cityId',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The id of city to return the weather of. List can be downloaded here: http://bulk.openweathermap.org/sample/',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Latitude',
|
||||
name: 'latitude',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '13.39',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
locationSelection: [
|
||||
'coordinates',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The latitude of the location to return the weather of.',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Longitude',
|
||||
name: 'longitude',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '52.52',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
locationSelection: [
|
||||
'coordinates',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The longitude of the location to return the weather of.',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Zip Code',
|
||||
name: 'zipCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '10115,de',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
locationSelection: [
|
||||
'zipCode',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The id of city to return the weather of. List can be downloaded here: http://bulk.openweathermap.org/sample/',
|
||||
},
|
||||
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const credentials = this.getCredentials('openWeatherMapApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
let endpoint = '';
|
||||
let locationSelection;
|
||||
|
||||
let qs: IDataObject;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// Set base data
|
||||
qs = {
|
||||
APPID: credentials.accessToken,
|
||||
units: this.getNodeParameter('format', 0) as string
|
||||
};
|
||||
|
||||
// Get the location
|
||||
locationSelection = this.getNodeParameter('locationSelection', 0) as string;
|
||||
if (locationSelection === 'cityName') {
|
||||
qs.q = this.getNodeParameter('cityName', 0) as string;
|
||||
} else if (locationSelection === 'cityId') {
|
||||
qs.id = this.getNodeParameter('cityId', 0) as number;
|
||||
} else if (locationSelection === 'coordinates') {
|
||||
qs.lat = this.getNodeParameter('latitude', 0) as string;
|
||||
qs.lon = this.getNodeParameter('longitude', 0) as string;
|
||||
} else if (locationSelection === 'zipCode') {
|
||||
qs.zip = this.getNodeParameter('zipCode', 0) as string;
|
||||
} else {
|
||||
throw new Error(`The locationSelection "${locationSelection}" is not known!`);
|
||||
}
|
||||
|
||||
|
||||
if (operation === 'currentWeather') {
|
||||
// ----------------------------------
|
||||
// currentWeather
|
||||
// ----------------------------------
|
||||
|
||||
endpoint = 'weather';
|
||||
} else if (operation === '5DayForecast') {
|
||||
// ----------------------------------
|
||||
// 5DayForecast
|
||||
// ----------------------------------
|
||||
|
||||
endpoint = 'forecast';
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not known!`);
|
||||
}
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
method: 'GET',
|
||||
qs,
|
||||
uri: `https://api.openweathermap.org/data/2.5/${endpoint}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
const responseData = await this.helpers.request(options);
|
||||
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
71
packages/nodes-base/nodes/ReadBinaryFile.node.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { IExecuteSingleFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeTypeDescription,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
|
||||
export class ReadBinaryFile implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Read Binary File',
|
||||
name: 'readBinaryFile',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Reads a binary file from disk',
|
||||
defaults: {
|
||||
name: 'Read Binary File',
|
||||
color: '#22CC33',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'File Path',
|
||||
name: 'filePath',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: '/data/example.jpg',
|
||||
description: 'Path of the file to read.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
description: 'Name of the binary property to which to<br />write the data of the read file.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const item = this.getInputData();
|
||||
|
||||
const dataPropertyName = this.getNodeParameter('dataPropertyName') as string;
|
||||
const filePath = this.getNodeParameter('filePath') as string;
|
||||
|
||||
if (item.binary === undefined) {
|
||||
item.binary = {};
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await fs.readFile(filePath) as Buffer;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new Error(`The file "${filePath}" could not be found.`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
item.binary[dataPropertyName] = await this.helpers.prepareBinaryData(data, filePath);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
}
|
||||
74
packages/nodes-base/nodes/ReadBinaryFiles.node.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as glob from 'glob-promise';
|
||||
import * as path from 'path';
|
||||
|
||||
export class ReadBinaryFiles implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Read Binary Files',
|
||||
name: 'readBinaryFiles',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Reads binary files from disk',
|
||||
defaults: {
|
||||
name: 'Read Binary Files',
|
||||
color: '#22CC33',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'File Selector',
|
||||
name: 'fileSelector',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
noDataExpression: true,
|
||||
placeholder: '*.jpg',
|
||||
description: 'Pattern for files to read.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
noDataExpression: true,
|
||||
description: 'Name of the binary property to which to<br />write the data of the read files.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const fileSelector = this.getNodeParameter('fileSelector', 0) as string;
|
||||
const dataPropertyName = this.getNodeParameter('dataPropertyName', 0) as string;
|
||||
|
||||
const files = await glob(fileSelector);
|
||||
|
||||
const items: INodeExecutionData[] = [];
|
||||
let item: INodeExecutionData;
|
||||
let data: Buffer;
|
||||
let fileName: string;
|
||||
for (const filePath of files) {
|
||||
data = await fs.readFile(filePath) as Buffer;
|
||||
|
||||
fileName = path.parse(filePath).base;
|
||||
item = {
|
||||
binary: {
|
||||
[dataPropertyName]: await this.helpers.prepareBinaryData(data, fileName)
|
||||
},
|
||||
json: {},
|
||||
};
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
80
packages/nodes-base/nodes/ReadFileFromUrl.node.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { IExecuteSingleFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
import { UriOptions } from 'request';
|
||||
|
||||
|
||||
export class ReadFileFromUrl implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Read File From Url',
|
||||
name: 'readFileFromUrl',
|
||||
icon: 'fa:globe',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Reads a file from an URL',
|
||||
defaults: {
|
||||
name: 'Read File URL',
|
||||
color: '#995533',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'http://example.com/index.html',
|
||||
description: 'URL of the file to read.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
description: 'Name of the binary property to which to<br />write the data of the URL.',
|
||||
},
|
||||
{
|
||||
displayName: 'Ignore SSL Issues',
|
||||
name: 'allowUnauthorizedCerts',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Still download the file even if SSL certificate validation is not possible.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const url = this.getNodeParameter('url') as string;
|
||||
const dataPropertyName = this.getNodeParameter('dataPropertyName') as string;
|
||||
|
||||
const options: UriOptions = {
|
||||
uri: url,
|
||||
// @ts-ignore
|
||||
encoding: null,
|
||||
rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', false) as boolean,
|
||||
};
|
||||
|
||||
// TODO: Should have a catch here
|
||||
const data = await this.helpers.request(options);
|
||||
|
||||
// Get the filename and also add it to the binary data
|
||||
const fileName = (url).split('/').pop();
|
||||
|
||||
// Get the current item and add the binary data
|
||||
const item = this.getInputData();
|
||||
|
||||
if (!item.binary) {
|
||||
item.binary = {};
|
||||
}
|
||||
|
||||
item.binary[dataPropertyName as string] = await this.helpers.prepareBinaryData(data, fileName);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
56
packages/nodes-base/nodes/ReadPdf.node.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteSingleFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
const pdf = require('pdf-parse');
|
||||
|
||||
export class ReadPdf implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Read PDF',
|
||||
name: 'Read PDF',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Reads a PDF and extracts its content',
|
||||
defaults: {
|
||||
name: 'Read PDF',
|
||||
color: '#003355',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
description: 'Name of the binary property from which to<br />read the PDF file.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName') as string;
|
||||
|
||||
const item = this.getInputData();
|
||||
|
||||
if (item.binary === undefined) {
|
||||
item.binary = {};
|
||||
}
|
||||
|
||||
const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING);
|
||||
|
||||
return {
|
||||
json: await pdf(binaryData)
|
||||
};
|
||||
}
|
||||
}
|
||||
458
packages/nodes-base/nodes/Redis/Redis.node.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
GenericValue,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { set } from 'lodash';
|
||||
import * as redis from 'redis';
|
||||
|
||||
import * as util from 'util';
|
||||
|
||||
export class Redis implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Redis',
|
||||
name: 'redis',
|
||||
icon: 'file:redis.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Gets, sends data to Redis and receives generic information.',
|
||||
defaults: {
|
||||
name: 'Redis',
|
||||
color: '#0033AA',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'redis',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Deletes a key from Redis.',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Returns the value of a key from Redis.',
|
||||
},
|
||||
{
|
||||
name: 'Info',
|
||||
value: 'info',
|
||||
description: 'Returns generic information about the Redis instance.',
|
||||
},
|
||||
{
|
||||
name: 'Keys',
|
||||
value: 'keys',
|
||||
description: 'Returns all the keys matching a pattern.',
|
||||
},
|
||||
{
|
||||
name: 'Set',
|
||||
value: 'set',
|
||||
description: 'Sets the value of a key in redis.',
|
||||
},
|
||||
],
|
||||
default: 'info',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// get
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'propertyName',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: 'propertyName',
|
||||
required: true,
|
||||
description: 'Name of the property to write received data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'delete'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the key to delete from Redis.',
|
||||
},
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the key to get from Redis.',
|
||||
},
|
||||
{
|
||||
displayName: 'Key Type',
|
||||
name: 'keyType',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get'
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Automatic',
|
||||
value: 'automatic',
|
||||
description: 'Requests the type before requesting the data (slower).',
|
||||
},
|
||||
{
|
||||
name: 'Hash',
|
||||
value: 'hash',
|
||||
description: 'Data in key is of type "hash".',
|
||||
},
|
||||
{
|
||||
name: 'String',
|
||||
value: 'string',
|
||||
description: 'Data in key is of type "string".',
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
value: 'list',
|
||||
description: 'Data in key is of type "lists".',
|
||||
},
|
||||
{
|
||||
name: 'Sets',
|
||||
value: 'sets',
|
||||
description: 'Data in key is of type "sets".',
|
||||
},
|
||||
],
|
||||
default: 'automatic',
|
||||
description: 'The type of the key to get.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// keys
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Key Pattern',
|
||||
name: 'keyPattern',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'keys'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'The key pattern for the keys to return.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// set
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'set'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the key to set in Redis.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'set'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'The value to write in Redis.',
|
||||
},
|
||||
{
|
||||
displayName: 'Key Type',
|
||||
name: 'keyType',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'set'
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Automatic',
|
||||
value: 'automatic',
|
||||
description: 'Tries to figure out the type automatically depending on the data.',
|
||||
},
|
||||
{
|
||||
name: 'Hash',
|
||||
value: 'hash',
|
||||
description: 'Data in key is of type "hash".',
|
||||
},
|
||||
{
|
||||
name: 'String',
|
||||
value: 'string',
|
||||
description: 'Data in key is of type "string".',
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
value: 'list',
|
||||
description: 'Data in key is of type "lists".',
|
||||
},
|
||||
{
|
||||
name: 'Sets',
|
||||
value: 'sets',
|
||||
description: 'Data in key is of type "sets".',
|
||||
},
|
||||
],
|
||||
default: 'automatic',
|
||||
description: 'The type of the key to set.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
// Parses the given value in a number if it is one else returns a string
|
||||
function getParsedValue (value: string): string | number {
|
||||
if (value.match(/^[\d\.]+$/) === null) {
|
||||
// Is a string
|
||||
return value;
|
||||
} else {
|
||||
// Is a number
|
||||
return parseFloat(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Converts the Redis Info String into an object
|
||||
function convertInfoToObject(stringData: string): IDataObject {
|
||||
const returnData: IDataObject = {};
|
||||
|
||||
let key:string, value:string;
|
||||
for (const line of stringData.split('\n')) {
|
||||
if (['#', ''].includes(line.charAt(0))) {
|
||||
continue;
|
||||
}
|
||||
[key, value] = line.split(':');
|
||||
if (key === undefined || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
value = value.trim();
|
||||
|
||||
if (value.includes('=')) {
|
||||
returnData[key] = {};
|
||||
let key2: string, value2: string;
|
||||
for (const keyValuePair of value.split(',')) {
|
||||
[key2, value2] = keyValuePair.split('=');
|
||||
(returnData[key] as IDataObject)[key2] = getParsedValue(value2);
|
||||
}
|
||||
} else {
|
||||
returnData[key] = getParsedValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
async function getValue(client: redis.RedisClient, keyName: string, type?: string) {
|
||||
if (type === undefined || type === 'automatic') {
|
||||
// Request the type first
|
||||
const clientType = util.promisify(client.type).bind(client);
|
||||
type = await clientType(keyName);
|
||||
}
|
||||
|
||||
console.log(keyName + ': ' + type);
|
||||
|
||||
|
||||
if (type === 'string') {
|
||||
const clientGet = util.promisify(client.get).bind(client);
|
||||
return await clientGet(keyName);
|
||||
} else if (type === 'hash') {
|
||||
const clientHGetAll = util.promisify(client.hgetall).bind(client);
|
||||
return await clientHGetAll(keyName);
|
||||
} else if (type === 'list') {
|
||||
const clientLRange = util.promisify(client.lrange).bind(client);
|
||||
return await clientLRange(keyName, 0, -1);
|
||||
} else if (type === 'sets') {
|
||||
const clientSMembers = util.promisify(client.smembers).bind(client);
|
||||
return await clientSMembers(keyName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function setValue(client: redis.RedisClient, keyName: string, value: string | number | object | string[] | number[], type?: string) {
|
||||
if (type === undefined || type === 'automatic') {
|
||||
// Request the type first
|
||||
if (typeof value === 'string') {
|
||||
type = 'string';
|
||||
} else if (Array.isArray(value)) {
|
||||
type = 'list';
|
||||
} else if (typeof value === 'object') {
|
||||
type = 'hash';
|
||||
} else {
|
||||
throw new Error('Could not identify the type to set. Please set it manually!');
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'string') {
|
||||
const clientSet = util.promisify(client.set).bind(client);
|
||||
return await clientSet(keyName, value.toString());
|
||||
} else if (type === 'hash') {
|
||||
const clientHset = util.promisify(client.hset).bind(client);
|
||||
for (const key of Object.keys(value)) {
|
||||
await clientHset(keyName, key, (value as IDataObject)[key]!.toString());
|
||||
}
|
||||
return;
|
||||
} else if (type === 'list') {
|
||||
const clientLset = util.promisify(client.lset).bind(client);
|
||||
for (let index = 0; index < (value as string[]).length; index++) {
|
||||
await clientLset(keyName, index, (value as IDataObject)[index]!.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// TODO: For array and object fields it should not have a "value" field it should
|
||||
// have a parameter field for a path. Because it is not possible to set
|
||||
// array, object via parameter directly (should maybe be possible?!?!)
|
||||
// Should maybe have a parameter which is JSON.
|
||||
const credentials = this.getCredentials('redis');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const redisOptions: redis.ClientOpts = {
|
||||
host: credentials.host as string,
|
||||
port: credentials.port as number,
|
||||
};
|
||||
|
||||
if (credentials.password) {
|
||||
redisOptions.password = credentials.password as string;
|
||||
}
|
||||
|
||||
const client = redis.createClient(redisOptions);
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
client.on('error', (err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
client.on('ready', async (err: Error | null) => {
|
||||
|
||||
if (operation === 'info') {
|
||||
const clientInfo = util.promisify(client.info).bind(client);
|
||||
const result = await clientInfo();
|
||||
|
||||
resolve(this.prepareOutputData([{ json: convertInfoToObject(result as unknown as string) }]));
|
||||
client.quit();
|
||||
|
||||
} else if (['delete', 'get', 'keys', 'set'].includes(operation)) {
|
||||
const items = this.getInputData();
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push({ json: {} });
|
||||
}
|
||||
|
||||
let item: INodeExecutionData;
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
item = items[itemIndex];
|
||||
|
||||
if (operation === 'delete') {
|
||||
const keyDelete = this.getNodeParameter('key', itemIndex) as string;
|
||||
|
||||
const clientDel = util.promisify(client.del).bind(client);
|
||||
// @ts-ignore
|
||||
await clientDel(keyDelete);
|
||||
|
||||
resolve(this.prepareOutputData(items));
|
||||
} else if (operation === 'get') {
|
||||
const propertyName = this.getNodeParameter('propertyName', itemIndex) as string;
|
||||
const keyGet = this.getNodeParameter('key', itemIndex) as string;
|
||||
const keyType = this.getNodeParameter('keyType', itemIndex) as string;
|
||||
|
||||
const value = await getValue(client, keyGet, keyType);
|
||||
set(item.json, propertyName, value);
|
||||
|
||||
resolve(this.prepareOutputData(items));
|
||||
} else if (operation === 'keys') {
|
||||
const keyPattern = this.getNodeParameter('keyPattern', itemIndex) as string;
|
||||
|
||||
const clientKeys = util.promisify(client.keys).bind(client);
|
||||
const keys = await clientKeys(keyPattern);
|
||||
|
||||
const promises: {
|
||||
[key: string]: GenericValue;
|
||||
} = {};
|
||||
|
||||
for (const keyName of keys) {
|
||||
promises[keyName] = await getValue(client, keyName);
|
||||
console.log(promises[keyName]);
|
||||
|
||||
}
|
||||
|
||||
for (const keyName of keys) {
|
||||
set(item.json, keyName, await promises[keyName]);
|
||||
}
|
||||
|
||||
resolve(this.prepareOutputData(items));
|
||||
} else if (operation === 'set') {
|
||||
const keySet = this.getNodeParameter('key', itemIndex) as string;
|
||||
const value = this.getNodeParameter('value', itemIndex) as string;
|
||||
const keyType = this.getNodeParameter('keyType', itemIndex) as string;
|
||||
|
||||
await setValue(client, keySet, value, keyType);
|
||||
|
||||
resolve(this.prepareOutputData(items));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Redis/redis.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
100
packages/nodes-base/nodes/RenameKeys.node.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
get,
|
||||
set,
|
||||
unset,
|
||||
} from 'lodash';
|
||||
|
||||
interface IRenameKey {
|
||||
currentKey: string;
|
||||
newKey: string;
|
||||
}
|
||||
|
||||
export class RenameKeys implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Rename Keys',
|
||||
name: 'renameKeys',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Renames keys.',
|
||||
defaults: {
|
||||
name: 'Rename Keys',
|
||||
color: '#772244',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Keys',
|
||||
name: 'keys',
|
||||
placeholder: 'Add new key',
|
||||
description: 'Adds a key which should be renamed.',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Current Key Name',
|
||||
name: 'currentKey',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'currentKey',
|
||||
description: 'The current name of the key. It is also possible to define deep keys by using dot-notation like for example: "level1.level2.currentKey"',
|
||||
},
|
||||
{
|
||||
displayName: 'New Key Name',
|
||||
name: 'newKey',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'newKey',
|
||||
description: 'the name the key should be renamed to. It is also possible to define deep keys by using dot-notation like for example: "level1.level2.newKey"',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
let item: INodeExecutionData;
|
||||
let renameKeys: IRenameKey[];
|
||||
let value: any; // tslint:disable-line:no-any
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
renameKeys = this.getNodeParameter('keys.key', itemIndex, []) as IRenameKey[];
|
||||
item = items[itemIndex];
|
||||
|
||||
renameKeys.forEach((renameKey) => {
|
||||
if (renameKey.currentKey === '' || renameKey.newKey === '') {
|
||||
// Ignore all which do not have all the values set
|
||||
return;
|
||||
}
|
||||
value = get(item.json, renameKey.currentKey as string);
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
set(item.json, renameKey.newKey, value);
|
||||
|
||||
unset(item.json, renameKey.currentKey as string);
|
||||
});
|
||||
}
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
74
packages/nodes-base/nodes/RssFeedRead.node.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as Parser from 'rss-parser';
|
||||
|
||||
export class RssFeedRead implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'RSS Read',
|
||||
name: 'rssFeedRead',
|
||||
icon: 'fa:rss',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Reads data from an RSS Feed',
|
||||
defaults: {
|
||||
name: 'RSS Feed Read',
|
||||
color: '#b02020',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
noDataExpression: true,
|
||||
description: 'URL of the RSS feed.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
|
||||
const url = this.getNodeParameter('url', 0) as string;
|
||||
|
||||
if (!url) {
|
||||
throw new Error('The parameter "URL" has to be set!');
|
||||
}
|
||||
// TODO: Later add also check if the url has a valid format
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
let feed: Parser.Output;
|
||||
try {
|
||||
feed = await parser.parseURL(url);
|
||||
} catch (e) {
|
||||
if (e.code === 'ECONNREFUSED') {
|
||||
throw new Error(`It was not possible to connect to the URL. Please make sure the URL "${url}" it is valid!`);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
// For now we just take the items and ignore everything else
|
||||
if (feed.items) {
|
||||
feed.items.forEach((item) => {
|
||||
// @ts-ignore
|
||||
returnData.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
145
packages/nodes-base/nodes/Set.node.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { set } from 'lodash';
|
||||
|
||||
export class Set implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets values on the items and removes if selected all other values.',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Keep Only Set',
|
||||
name: 'keepOnlySet',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'If only the values set on this node should be<br />kept and all others removed.',
|
||||
},
|
||||
{
|
||||
displayName: 'Values to Set',
|
||||
name: 'values',
|
||||
placeholder: 'Add Value',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
description: 'The value to set.',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'boolean',
|
||||
displayName: 'Boolean',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'The boolean value to write in the property.',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The number value to write in the property.',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'propertyName',
|
||||
description: 'Name of the property to write data to.<br />Supports dot-notation.<br />Example: "data.person[0].name"',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The string value to write in the property.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push({json: {}});
|
||||
}
|
||||
|
||||
let item: INodeExecutionData;
|
||||
let keepOnlySet: boolean;
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, []) as boolean;
|
||||
item = items[itemIndex];
|
||||
|
||||
if (keepOnlySet === true) {
|
||||
item.json = {};
|
||||
}
|
||||
|
||||
// Add boolean values
|
||||
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
|
||||
set(item.json, setItem.name as string, !!setItem.value);
|
||||
});
|
||||
|
||||
// Add number values
|
||||
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
|
||||
set(item.json, setItem.name as string, setItem.value);
|
||||
});
|
||||
|
||||
// Add string values
|
||||
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
|
||||
set(item.json, setItem.name as string, setItem.value);
|
||||
});
|
||||
}
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
574
packages/nodes-base/nodes/Slack/Slack.node.ts
Normal file
@@ -0,0 +1,574 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
interface Attachment {
|
||||
fields: {
|
||||
item?: object[];
|
||||
};
|
||||
}
|
||||
|
||||
export class Slack implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Slack',
|
||||
name: 'slack',
|
||||
icon: 'file:slack.png',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
description: 'Sends data to Slack',
|
||||
defaults: {
|
||||
name: 'Slack',
|
||||
color: '#BB2244',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'slackApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Create Channel',
|
||||
value: 'channelsCreate',
|
||||
description: 'Creates a new channel',
|
||||
},
|
||||
{
|
||||
name: 'Invite User',
|
||||
value: 'channelsInvite',
|
||||
description: 'Invites a user to a channel',
|
||||
},
|
||||
{
|
||||
name: 'Send Message',
|
||||
value: 'chatPostMessage',
|
||||
description: 'Posts a message into a channel',
|
||||
},
|
||||
],
|
||||
default: 'chatPostMessage',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// channelsCreate
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'channel',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'Channel name',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'channelsCreate'
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
description: 'The name of the channel to create.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// channelsInvite
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Channel ID',
|
||||
name: 'channel',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'myChannel',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'channelsInvite'
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
description: 'The ID of the channel to invite user to.',
|
||||
},
|
||||
{
|
||||
displayName: 'User ID',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'frank',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'channelsInvite'
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
description: 'The ID of the user to invite into channel.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// chatPostMessage
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Channel',
|
||||
name: 'channel',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'Channel name',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'chatPostMessage'
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
description: 'The channel to send the message to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'chatPostMessage'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The text to send.',
|
||||
},
|
||||
{
|
||||
displayName: 'As User',
|
||||
name: 'as_user',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'chatPostMessage'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Post the message as authenticated user instead of bot.',
|
||||
},
|
||||
{
|
||||
displayName: 'User Name',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'chatPostMessage'
|
||||
],
|
||||
as_user: [
|
||||
false
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Set the bot\'s user name.',
|
||||
},
|
||||
{
|
||||
displayName: 'Attachments',
|
||||
name: 'attachments',
|
||||
type: 'collection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
multipleValueButtonText: 'Add attachment',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'chatPostMessage'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI
|
||||
description: 'The attachment to add',
|
||||
placeholder: 'Add attachment item',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Fallback Text',
|
||||
name: 'fallback',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Required plain-text summary of the attachment.',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Text to send.',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Title of the message.',
|
||||
},
|
||||
{
|
||||
displayName: 'Title Link',
|
||||
name: 'title_link',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Link of the title.',
|
||||
},
|
||||
{
|
||||
displayName: 'Color',
|
||||
name: 'color',
|
||||
type: 'color',
|
||||
default: '#ff0000',
|
||||
description: 'Color of the line left of text.',
|
||||
},
|
||||
{
|
||||
displayName: 'Pretext',
|
||||
name: 'pretext',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Text which appears before the message block.',
|
||||
},
|
||||
{
|
||||
displayName: 'Author Name',
|
||||
name: 'author_name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name that should appear.',
|
||||
},
|
||||
{
|
||||
displayName: 'Author Link',
|
||||
name: 'author_link',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Link for the author.',
|
||||
},
|
||||
{
|
||||
displayName: 'Author Icon',
|
||||
name: 'author_icon',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Icon which should appear for the user.',
|
||||
},
|
||||
{
|
||||
displayName: 'Image URL',
|
||||
name: 'image_url',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'URL of image.',
|
||||
},
|
||||
{
|
||||
displayName: 'Thumbnail URL',
|
||||
name: 'thumb_url',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'URL of thumbnail.',
|
||||
},
|
||||
{
|
||||
displayName: 'Footer',
|
||||
name: 'footer',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Text of footer to add.',
|
||||
},
|
||||
{
|
||||
displayName: 'Footer Icon',
|
||||
name: 'footer_icon',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Icon which should appear next to footer.',
|
||||
},
|
||||
{
|
||||
displayName: 'Timestamp',
|
||||
name: 'ts',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Time message relates to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Fields',
|
||||
name: 'fields',
|
||||
placeholder: 'Add Fields',
|
||||
description: 'Fields to add to message.',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'item',
|
||||
displayName: 'Item',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Title of the item.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value of the item.',
|
||||
},
|
||||
{
|
||||
displayName: 'Short',
|
||||
name: 'short',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'If items can be displayed next to each other.',
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Other Options',
|
||||
name: 'otherOptions',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'chatPostMessage'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
description: 'Other options to set',
|
||||
placeholder: 'Add options',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Icon Emoji',
|
||||
name: 'icon_emoji',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/as_user': [
|
||||
false
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Emoji to use as the icon for this message. Overrides icon_url.',
|
||||
},
|
||||
{
|
||||
displayName: 'Icon URL',
|
||||
name: 'icon_url',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/as_user': [
|
||||
false
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'URL to an image to use as the icon for this message.',
|
||||
},
|
||||
{
|
||||
displayName: 'Make Reply',
|
||||
name: 'thread_ts',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Provide another message\'s ts value to make this message a reply.',
|
||||
},
|
||||
{
|
||||
displayName: 'Unfurl Links',
|
||||
name: 'unfurl_links',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Pass true to enable unfurling of primarily text-based content.',
|
||||
},
|
||||
{
|
||||
displayName: 'Unfurl Media',
|
||||
name: 'unfurl_media',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Pass false to disable unfurling of media content.',
|
||||
},
|
||||
{
|
||||
displayName: 'Markdown',
|
||||
name: 'mrkdwn',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Use Slack Markdown parsing.',
|
||||
},
|
||||
{
|
||||
displayName: 'Reply Broadcast',
|
||||
name: 'reply_broadcast',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.',
|
||||
},
|
||||
{
|
||||
displayName: 'Link Names',
|
||||
name: 'link_names',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Find and link channel names and usernames.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const credentials = this.getCredentials('slackApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const baseUrl = `https://slack.com/api/`;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
let requestMethod = 'GET';
|
||||
|
||||
// For Post
|
||||
let body: IDataObject;
|
||||
// For Query string
|
||||
let qs: IDataObject;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let endpoint = '';
|
||||
body = {};
|
||||
qs = {};
|
||||
|
||||
if (operation === 'channelsCreate') {
|
||||
// ----------------------------------
|
||||
// channelsCreate
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
endpoint = 'channels.create';
|
||||
|
||||
body.name = this.getNodeParameter('channel', i) as string;
|
||||
} else if (operation === 'chatPostMessage') {
|
||||
// ----------------------------------
|
||||
// chatPostMessage
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
endpoint = 'chat.postMessage';
|
||||
|
||||
body.channel = this.getNodeParameter('channel', i) as string;
|
||||
body.text = this.getNodeParameter('text', i) as string;
|
||||
body.as_user = this.getNodeParameter('as_user', i) as boolean;
|
||||
if (body.as_user === false) {
|
||||
body.username = this.getNodeParameter('username', i) as string;
|
||||
}
|
||||
|
||||
const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[];
|
||||
|
||||
// The node does save the fields data differently than the API
|
||||
// expects so fix the data befre we send the request
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.fields !== undefined) {
|
||||
if (attachment.fields.item !== undefined) {
|
||||
// Move the field-content up
|
||||
// @ts-ignore
|
||||
attachment.fields = attachment.fields.item;
|
||||
} else {
|
||||
// If it does not have any items set remove it
|
||||
delete attachment.fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
body['attachments'] = attachments;
|
||||
|
||||
// Add all the other options to the request
|
||||
const otherOptions = this.getNodeParameter('otherOptions', i) as IDataObject;
|
||||
Object.assign(body, otherOptions);
|
||||
} else if (operation === 'channelsInvite') {
|
||||
// ----------------------------------
|
||||
// channelsInvite
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
endpoint = 'channels.invite';
|
||||
|
||||
body.channel = this.getNodeParameter('channel', i) as string;
|
||||
body.user = this.getNodeParameter('username', i) as string;
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: requestMethod,
|
||||
body,
|
||||
qs,
|
||||
uri: `${baseUrl}/${endpoint}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${credentials.accessToken }`,
|
||||
'content-type': 'application/json; charset=utf-8'
|
||||
},
|
||||
json: true
|
||||
};
|
||||
|
||||
const responseData = await this.helpers.request(options);
|
||||
|
||||
if (!responseData.ok) {
|
||||
throw new Error(`Request to Slack did fail with error: "${responseData.error}"`);
|
||||
}
|
||||
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Slack/slack.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
71
packages/nodes-base/nodes/SplitInBatches.node.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class SplitInBatches implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Split In Batches',
|
||||
name: 'splitInBatches',
|
||||
group: ['organization'],
|
||||
version: 1,
|
||||
description: 'Saves the originally incoming data and with each itteration it returns a predefined amount of them.',
|
||||
defaults: {
|
||||
name: 'SplitInBatches',
|
||||
color: '#007755',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Batch Size',
|
||||
name: 'batchSize',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 10,
|
||||
description: 'The number of items to return with each call.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][] | null> {
|
||||
const items = this.getInputData();
|
||||
|
||||
const nodeContext = this.getContext('node');
|
||||
|
||||
const batchSize = this.getNodeParameter('batchSize', 0) as number;
|
||||
|
||||
const returnItems: INodeExecutionData[] = [];
|
||||
|
||||
if (nodeContext.items === undefined) {
|
||||
// Is the first time the node runs
|
||||
|
||||
nodeContext.currentRunIndex = 0;
|
||||
nodeContext.maxRunIndex = Math.ceil(items.length / batchSize);
|
||||
|
||||
// Get the items which should be returned
|
||||
returnItems.push.apply(returnItems, items.splice(0, batchSize));
|
||||
|
||||
// Set the other items to be saved in the context to return at later runs
|
||||
nodeContext.items = items;
|
||||
} else {
|
||||
// The node has been called before. So return the next batch of items.
|
||||
nodeContext.currentRunIndex += 1;
|
||||
returnItems.push.apply(returnItems, nodeContext.items.splice(0, batchSize));
|
||||
}
|
||||
|
||||
nodeContext.noItemsLeft = nodeContext.items.length === 0;
|
||||
|
||||
if (returnItems.length === 0) {
|
||||
// No data left to return so stop execution of the branch
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnItems);
|
||||
}
|
||||
}
|
||||
271
packages/nodes-base/nodes/SpreadsheetFile.node.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
read as xlsxRead,
|
||||
utils as xlsxUtils,
|
||||
write as xlsxWrite,
|
||||
WritingOptions,
|
||||
WorkBook,
|
||||
} from 'xlsx';
|
||||
|
||||
|
||||
/**
|
||||
* Flattens an object with deep data
|
||||
*
|
||||
* @param {IDataObject} data The object to flatten
|
||||
* @returns
|
||||
*/
|
||||
function flattenObject (data: IDataObject) {
|
||||
const returnData: IDataObject = {};
|
||||
for (const key1 of Object.keys(data)) {
|
||||
if ((typeof data[key1]) === 'object') {
|
||||
const flatObject = flattenObject(data[key1] as IDataObject);
|
||||
for (const key2 in flatObject) {
|
||||
if (flatObject[key2] === undefined) {
|
||||
continue;
|
||||
}
|
||||
returnData[`${key1}.${key2}`] = flatObject[key2];
|
||||
}
|
||||
} else {
|
||||
returnData[key1] = data[key1];
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
|
||||
export class SpreadsheetFile implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Spreadsheet File',
|
||||
name: 'spreadsheetFile',
|
||||
icon: 'fa:table',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Reads and writes data from a spreadsheet file.',
|
||||
defaults: {
|
||||
name: 'Spreadsheet File',
|
||||
color: '#2244FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Read from file',
|
||||
value: 'fromFile',
|
||||
description: 'Reads data from a spreadsheet file',
|
||||
},
|
||||
{
|
||||
name: 'Write to file',
|
||||
value: 'toFile',
|
||||
description: 'Writes the workflow data to a spreadsheet file',
|
||||
},
|
||||
],
|
||||
default: 'fromFile',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// fromFile
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'fromFile',
|
||||
],
|
||||
},
|
||||
|
||||
},
|
||||
placeholder: '',
|
||||
description: 'Name of the binary property from which to read<br />the binary data of the spreadsheet file.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// toFile
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'File Format',
|
||||
name: 'fileFormat',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'csv',
|
||||
value: 'csv',
|
||||
description: 'Comma-separated values',
|
||||
},
|
||||
{
|
||||
name: 'ods',
|
||||
value: 'ods',
|
||||
description: 'OpenDocument Spreadsheet',
|
||||
},
|
||||
{
|
||||
name: 'rtf',
|
||||
value: 'rtf',
|
||||
description: 'Rich Text Format',
|
||||
},
|
||||
{
|
||||
name: 'html',
|
||||
value: 'html',
|
||||
description: 'HTML Table',
|
||||
},
|
||||
{
|
||||
name: 'xls',
|
||||
value: 'xls',
|
||||
description: 'Excel',
|
||||
},
|
||||
],
|
||||
default: 'xls',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'toFile'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The format of the file to save the data as.',
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Property',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'toFile',
|
||||
],
|
||||
},
|
||||
|
||||
},
|
||||
placeholder: '',
|
||||
description: 'Name of the binary property in which to save<br />the binary data of the spreadsheet file.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
const newItems: INodeExecutionData[] = [];
|
||||
|
||||
if (operation === 'fromFile') {
|
||||
// Read data from spreadsheet file to workflow
|
||||
|
||||
let item: INodeExecutionData;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
item = items[i];
|
||||
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
|
||||
|
||||
if (item.binary === undefined || item.binary[binaryPropertyName] === undefined) {
|
||||
// Property did not get found on item
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read the binary spreadsheet data
|
||||
const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING);
|
||||
const workbook = xlsxRead(binaryData);
|
||||
|
||||
if (workbook.SheetNames.length === 0) {
|
||||
throw new Error('File does not have any sheets!');
|
||||
}
|
||||
|
||||
// Convert it to json
|
||||
const sheetJson = xlsxUtils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]);
|
||||
|
||||
// Check if data could be found in file
|
||||
if (sheetJson.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add all the found data columns to the workflow data
|
||||
for (const rowData of sheetJson) {
|
||||
newItems.push({ json: rowData } as INodeExecutionData);
|
||||
}
|
||||
}
|
||||
|
||||
return this.prepareOutputData(newItems);
|
||||
} else if (operation === 'toFile') {
|
||||
// Write the workflow data to spreadsheet file
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
|
||||
const fileFormat = this.getNodeParameter('fileFormat', 0) as string;
|
||||
|
||||
// Get the json data of the items and flatten it
|
||||
let item: INodeExecutionData;
|
||||
const itemData: IDataObject[] = [];
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
item = items[itemIndex];
|
||||
itemData.push(flattenObject(item.json));
|
||||
}
|
||||
|
||||
const ws = xlsxUtils.json_to_sheet(itemData);
|
||||
|
||||
const wopts: WritingOptions = {
|
||||
bookSST: false,
|
||||
type: 'buffer'
|
||||
};
|
||||
|
||||
if (fileFormat === 'csv') {
|
||||
wopts.bookType = 'csv';
|
||||
} else if (fileFormat === 'html') {
|
||||
wopts.bookType = 'html';
|
||||
} else if (fileFormat === 'rtf') {
|
||||
wopts.bookType = 'rtf';
|
||||
} else if (fileFormat === 'ods') {
|
||||
wopts.bookType = 'ods';
|
||||
} else if (fileFormat === 'xls') {
|
||||
wopts.bookType = 'xlml';
|
||||
}
|
||||
|
||||
// Convert the data in the correct format
|
||||
const sheetName = 'Sheet';
|
||||
const wb: WorkBook = {
|
||||
SheetNames: [sheetName],
|
||||
Sheets: {
|
||||
[sheetName]: ws,
|
||||
}
|
||||
};
|
||||
const wbout = xlsxWrite(wb, wopts);
|
||||
|
||||
// Create a new item with only the binary spreadsheet data
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
binary: {},
|
||||
};
|
||||
|
||||
newItem.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(wbout, `spreadsheet.${fileFormat}`);
|
||||
|
||||
const newItems = [];
|
||||
newItems.push(newItem);
|
||||
|
||||
return this.prepareOutputData(newItems);
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not supported!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
packages/nodes-base/nodes/Start.node.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class Start implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Start',
|
||||
name: 'start',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow execution from this node',
|
||||
maxNodes: 1,
|
||||
defaults: {
|
||||
name: 'Start',
|
||||
color: '#553399',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
],
|
||||
};
|
||||
|
||||
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
return this.prepareOutputData(items);
|
||||
}
|
||||
}
|
||||
62
packages/nodes-base/nodes/Twilio/GenericFunctions.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Make an API request to Twilio
|
||||
*
|
||||
* @param {IHookFunctions} this
|
||||
* @param {string} method
|
||||
* @param {string} url
|
||||
* @param {object} body
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function twilioApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('twilioApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
if (query === undefined) {
|
||||
query = {};
|
||||
}
|
||||
|
||||
const options = {
|
||||
method,
|
||||
form: body,
|
||||
qs: query,
|
||||
uri: `https://api.twilio.com/2010-04-01/Accounts/${credentials.accountSid}${endpoint}`,
|
||||
auth: {
|
||||
user: credentials.accountSid as string,
|
||||
pass: credentials.authToken as string,
|
||||
},
|
||||
json: true
|
||||
};
|
||||
|
||||
try {
|
||||
return this.helpers.request(options);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 401) {
|
||||
// Return a clear error
|
||||
throw new Error('The Twilio credentials are not valid!');
|
||||
}
|
||||
|
||||
if (error.response && error.response.body && error.response.body.message) {
|
||||
// Try to return the error prettier
|
||||
let errorMessage = `Twilio error response [${error.statusCode}]: ${error.response.body.message}`;
|
||||
if (error.response.body.more_info) {
|
||||
errorMessage = `errorMessage (${error.response.body.more_info})`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// If that data does not exist for some reason return the actual error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
171
packages/nodes-base/nodes/Twilio/Twilio.node.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
twilioApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
export class Twilio implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Twilio',
|
||||
name: 'twilio',
|
||||
icon: 'file:twilio.png',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Send SMS and WhatsApp messages or make phone calls',
|
||||
defaults: {
|
||||
name: 'Twilio',
|
||||
color: '#cf272d',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'twilioApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Send Message',
|
||||
value: 'sendMessage',
|
||||
description: 'Send SMS/MMS/WhatsApp message',
|
||||
},
|
||||
],
|
||||
default: 'sendMessage',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// sendMessage
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'From',
|
||||
name: 'from',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '+14155238886',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'sendMessage',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The number from which to send the message',
|
||||
},
|
||||
{
|
||||
displayName: 'To',
|
||||
name: 'to',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '+14155238886',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'sendMessage',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The number to which to send the message',
|
||||
},
|
||||
{
|
||||
displayName: 'To Whatsapp',
|
||||
name: 'toWhatsapp',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'sendMessage',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'If the message should be send to WhatsApp',
|
||||
},
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'sendMessage',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The message to send',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
// For Post
|
||||
let body: IDataObject;
|
||||
// For Query string
|
||||
let qs: IDataObject;
|
||||
|
||||
let requestMethod: string;
|
||||
let endpoint: string;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
requestMethod = 'GET';
|
||||
endpoint = '';
|
||||
body = {};
|
||||
qs = {};
|
||||
|
||||
if (operation === 'sendMessage') {
|
||||
// ----------------------------------
|
||||
// sendMessage
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
endpoint = '/Messages.json';
|
||||
|
||||
body.From = this.getNodeParameter('from', i) as string;
|
||||
body.To = this.getNodeParameter('to', i) as string;
|
||||
body.Body = this.getNodeParameter('message', i) as string;
|
||||
|
||||
const toWhatsapp = this.getNodeParameter('toWhatsapp', i) as boolean;
|
||||
|
||||
if (toWhatsapp === true) {
|
||||
body.From = `whatsapp:${body.From}`;
|
||||
body.To = `whatsapp:${body.To}`;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not known!`);
|
||||
}
|
||||
|
||||
const responseData = await twilioApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Twilio/twilio.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
258
packages/nodes-base/nodes/Webhook.node.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeType,
|
||||
IWebhookResonseData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as basicAuth from 'basic-auth';
|
||||
|
||||
import { Response } from 'express';
|
||||
|
||||
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
|
||||
if (message === undefined) {
|
||||
message = 'Authorization problem!';
|
||||
if (responseCode === 401) {
|
||||
message = 'Authorization is required!';
|
||||
} else if (responseCode === 403) {
|
||||
message = 'Authorization data is wrong!';
|
||||
}
|
||||
}
|
||||
|
||||
resp.writeHead(responseCode, { 'WWW-Authenticate': `Basic realm="${realm}"` });
|
||||
resp.end(message);
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
export class Webhook implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Webhook',
|
||||
name: 'webhook',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when a webhook got called.',
|
||||
defaults: {
|
||||
name: 'Webhook',
|
||||
color: '#885577',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'httpBasicAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'basicAuth',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'httpHeaderAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'headerAuth',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: '={{$parameter["httpMethod"]}}',
|
||||
reponseMode: '={{$parameter["reponseMode"]}}',
|
||||
reponseData: '={{$parameter["reponseData"]}}',
|
||||
responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}',
|
||||
path: '={{$parameter["path"]}}',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Basic Auth',
|
||||
value: 'basicAuth'
|
||||
},
|
||||
{
|
||||
name: 'Header Auth',
|
||||
value: 'headerAuth'
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none'
|
||||
},
|
||||
],
|
||||
default: 'none',
|
||||
description: 'The way to authenticate.',
|
||||
},
|
||||
{
|
||||
displayName: 'HTTP Method',
|
||||
name: 'httpMethod',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'GET',
|
||||
value: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'POST',
|
||||
value: 'POST',
|
||||
},
|
||||
],
|
||||
default: 'GET',
|
||||
description: 'The HTTP method to liste to.',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
required: true,
|
||||
description: 'The path to listen to',
|
||||
},
|
||||
{
|
||||
displayName: 'Reponse Mode',
|
||||
name: 'reponseMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'On Received',
|
||||
value: 'onReceived',
|
||||
description: 'Returns directly with Reponse Code 200',
|
||||
},
|
||||
{
|
||||
name: 'Last Node',
|
||||
value: 'lastNode',
|
||||
description: 'Returns data of the last executed node',
|
||||
},
|
||||
],
|
||||
default: 'onReceived',
|
||||
description: 'When and how to respond to the webhook.',
|
||||
},
|
||||
{
|
||||
displayName: 'Reponse Data',
|
||||
name: 'reponseData',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
reponseMode: [
|
||||
'lastNode',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'All Entries',
|
||||
value: 'allEntries',
|
||||
description: 'Returns all the entries of the last node. Always returns an array.',
|
||||
},
|
||||
{
|
||||
name: 'First Entry JSON',
|
||||
value: 'firstEntryJson',
|
||||
description: 'Returns the JSON data of the first entry of the last node. Always returns a JSON object.',
|
||||
},
|
||||
{
|
||||
name: 'First Entry Binary',
|
||||
value: 'firstEntryBinary',
|
||||
description: 'Returns the binary data of the first entry of the last node. Always returns a binary file.',
|
||||
},
|
||||
],
|
||||
default: 'firstEntryJson',
|
||||
description: 'What data should be returned. If it should return<br />all the itemsas array or only the first item as object.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'responseBinaryPropertyName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
reponseData: [
|
||||
'firstEntryBinary'
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Name of the binary property to return',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResonseData> {
|
||||
const authentication = this.getNodeParameter('authentication', 0) as string;
|
||||
const req = this.getRequestObject();
|
||||
const resp = this.getResponseObject();
|
||||
const headers = this.getHeaderData();
|
||||
const realm = 'Webhook';
|
||||
|
||||
if (authentication === 'basicAuth') {
|
||||
// Basic authorization is needed to call webhook
|
||||
const httpBasicAuth = this.getCredentials('httpBasicAuth');
|
||||
|
||||
if (httpBasicAuth === undefined || !httpBasicAuth.user || !httpBasicAuth.password) {
|
||||
// Data is not defined on node so can not authenticate
|
||||
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
|
||||
}
|
||||
|
||||
const basicAuthData = basicAuth(req);
|
||||
|
||||
if (basicAuthData === undefined) {
|
||||
// Authorization data is missing
|
||||
return authorizationError(resp, realm, 401);
|
||||
}
|
||||
|
||||
if (basicAuthData.name !== httpBasicAuth!.user || basicAuthData.pass !== httpBasicAuth!.password) {
|
||||
// Provided authentication data is wrong
|
||||
return authorizationError(resp, realm, 403);
|
||||
}
|
||||
} else if (authentication === 'headerAuth') {
|
||||
// Special header with value is needed to call webhook
|
||||
const httpHeaderAuth = this.getCredentials('httpHeaderAuth');
|
||||
|
||||
if (httpHeaderAuth === undefined || !httpHeaderAuth.name || !httpHeaderAuth.value) {
|
||||
// Data is not defined on node so can not authenticate
|
||||
return authorizationError(resp, realm, 500, 'No authentication data defined on node!');
|
||||
}
|
||||
const headerName = (httpHeaderAuth.name as string).toLowerCase();
|
||||
const headerValue = (httpHeaderAuth.value as string);
|
||||
|
||||
if (!headers.hasOwnProperty(headerName) || (headers as IDataObject)[headerName] !== headerValue) {
|
||||
// Provided authentication data is wrong
|
||||
return authorizationError(resp, realm, 403);
|
||||
}
|
||||
}
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
returnData.push(
|
||||
{
|
||||
body: this.getBodyData(),
|
||||
headers,
|
||||
query: this.getQueryData(),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
workflowData: [
|
||||
this.helpers.returnJsonArray(returnData)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
76
packages/nodes-base/nodes/WriteBinaryFile.node.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteSingleFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
|
||||
export class WriteBinaryFile implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Write Binary File',
|
||||
name: 'writeBinaryFile',
|
||||
icon: 'fa:save',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
description: 'Writes a binary file to disk',
|
||||
defaults: {
|
||||
name: 'Write Binary File',
|
||||
color: '#CC2233',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'fileName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: '/data/example.jpg',
|
||||
description: 'Path to which the file should be written.',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
description: 'Name of the binary property which contains<br />the data for the file to be written.',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const item = this.getInputData();
|
||||
|
||||
const dataPropertyName = this.getNodeParameter('dataPropertyName') as string;
|
||||
const fileName = this.getNodeParameter('fileName') as string;
|
||||
|
||||
if (item.binary === undefined) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (item.binary[dataPropertyName] === undefined) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// Write the file to disk
|
||||
await fs.writeFile(fileName, Buffer.from(item.binary[dataPropertyName].data, BINARY_ENCODING), 'binary');
|
||||
|
||||
if (item.json === undefined) {
|
||||
item.json = {};
|
||||
}
|
||||
|
||||
// Add the file name to data
|
||||
(item.json as IDataObject).fileName = fileName;
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
139
packages/nodes-base/package.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"name": "n8n-nodes-base",
|
||||
"version": "0.1.0",
|
||||
"description": "Base nodes of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"main": "dist/src/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc && gulp",
|
||||
"tslint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"watch": "tsc --watch",
|
||||
"test": "jest"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"n8n": {
|
||||
"credentials": [
|
||||
"dist/credentials/AsanaApi.credentials.js",
|
||||
"dist/credentials/ChargebeeApi.credentials.js",
|
||||
"dist/credentials/DropboxApi.credentials.js",
|
||||
"dist/credentials/GithubApi.credentials.js",
|
||||
"dist/credentials/GoogleApi.credentials.js",
|
||||
"dist/credentials/HttpBasicAuth.credentials.js",
|
||||
"dist/credentials/HttpHeaderAuth.credentials.js",
|
||||
"dist/credentials/Imap.credentials.js",
|
||||
"dist/credentials/LinkFishApi.credentials.js",
|
||||
"dist/credentials/MailgunApi.credentials.js",
|
||||
"dist/credentials/NextCloudApi.credentials.js",
|
||||
"dist/credentials/OpenWeatherMapApi.credentials.js",
|
||||
"dist/credentials/Redis.credentials.js",
|
||||
"dist/credentials/SlackApi.credentials.js",
|
||||
"dist/credentials/Smtp.credentials.js",
|
||||
"dist/credentials/TwilioApi.credentials.js"
|
||||
],
|
||||
"nodes": [
|
||||
"dist/nodes/Asana/Asana.node.js",
|
||||
"dist/nodes/Asana/AsanaTrigger.node.js",
|
||||
"dist/nodes/Chargebee/Chargebee.node.js",
|
||||
"dist/nodes/Chargebee/ChargebeeTrigger.node.js",
|
||||
"dist/nodes/Cron.node.js",
|
||||
"dist/nodes/Dropbox/Dropbox.node.js",
|
||||
"dist/nodes/EditImage.node.js",
|
||||
"dist/nodes/EmailReadImap.node.js",
|
||||
"dist/nodes/EmailSend.node.js",
|
||||
"dist/nodes/ErrorTrigger.node.js",
|
||||
"dist/nodes/ExecuteCommand.node.js",
|
||||
"dist/nodes/Function.node.js",
|
||||
"dist/nodes/FunctionItem.node.js",
|
||||
"dist/nodes/Github/Github.node.js",
|
||||
"dist/nodes/Github/GithubTrigger.node.js",
|
||||
"dist/nodes/GoogleSheets/GoogleSheets.node.js",
|
||||
"dist/nodes/HttpRequest.node.js",
|
||||
"dist/nodes/If.node.js",
|
||||
"dist/nodes/Interval.node.js",
|
||||
"dist/nodes/LinkFish/LinkFish.node.js",
|
||||
"dist/nodes/Mailgun.node.js",
|
||||
"dist/nodes/Merge.node.js",
|
||||
"dist/nodes/NextCloud/NextCloud.node.js",
|
||||
"dist/nodes/NoOp.node.js",
|
||||
"dist/nodes/OpenWeatherMap.node.js",
|
||||
"dist/nodes/ReadBinaryFile.node.js",
|
||||
"dist/nodes/ReadBinaryFiles.node.js",
|
||||
"dist/nodes/Redis/Redis.node.js",
|
||||
"dist/nodes/ReadFileFromUrl.node.js",
|
||||
"dist/nodes/ReadPdf.node.js",
|
||||
"dist/nodes/RenameKeys.node.js",
|
||||
"dist/nodes/RssFeedRead.node.js",
|
||||
"dist/nodes/Set.node.js",
|
||||
"dist/nodes/SplitInBatches.node.js",
|
||||
"dist/nodes/Slack/Slack.node.js",
|
||||
"dist/nodes/SpreadsheetFile.node.js",
|
||||
"dist/nodes/Start.node.js",
|
||||
"dist/nodes/Twilio/Twilio.node.js",
|
||||
"dist/nodes/WriteBinaryFile.node.js",
|
||||
"dist/nodes/Webhook.node.js"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/basic-auth": "^1.1.2",
|
||||
"@types/cron": "^1.6.1",
|
||||
"@types/express": "^4.16.1",
|
||||
"@types/gm": "^1.18.2",
|
||||
"@types/imap-simple": "^4.2.0",
|
||||
"@types/jest": "^23.3.2",
|
||||
"@types/lodash.set": "^4.3.6",
|
||||
"@types/node": "^10.10.1",
|
||||
"@types/nodemailer": "^4.6.5",
|
||||
"@types/redis": "^2.8.11",
|
||||
"@types/request-promise-native": "^1.0.15",
|
||||
"@types/xml2js": "^0.4.3",
|
||||
"gulp": "^4.0.0",
|
||||
"jest": "^23.6.0",
|
||||
"n8n-core": "^0.1.0",
|
||||
"n8n-workflow": "^0.1.0",
|
||||
"ts-jest": "^23.10.1",
|
||||
"tslint": "^5.11.0",
|
||||
"typescript": "~3.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"basic-auth": "^2.0.1",
|
||||
"cron": "^1.6.0",
|
||||
"glob-promise": "^3.4.0",
|
||||
"gm": "^1.23.1",
|
||||
"googleapis": "^36.0.0",
|
||||
"imap-simple": "^4.3.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.unset": "^4.5.2",
|
||||
"nodemailer": "^5.1.1",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"redis": "^2.8.0",
|
||||
"rss-parser": "^3.7.0",
|
||||
"vm2": "^3.6.10",
|
||||
"xlsx": "^0.14.3",
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"testURL": "http://localhost/",
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"testPathIgnorePatterns": [
|
||||
"/dist/",
|
||||
"/node_modules/"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"json"
|
||||
]
|
||||
}
|
||||
}
|
||||
356
packages/nodes-base/src/GoogleSheet.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import { google } from 'googleapis';
|
||||
import { JWT } from 'google-auth-library';
|
||||
|
||||
const Sheets = google.sheets('v4'); // tslint:disable-line:variable-name
|
||||
|
||||
export interface ISheetOptions {
|
||||
scope: string[];
|
||||
}
|
||||
|
||||
export interface IGoogleAuthCredentials {
|
||||
email: string;
|
||||
privateKey: string;
|
||||
}
|
||||
|
||||
export interface ISheetUpdateData {
|
||||
range: string;
|
||||
values: string[][];
|
||||
}
|
||||
|
||||
|
||||
export class GoogleSheet {
|
||||
id: string;
|
||||
credentials: IGoogleAuthCredentials;
|
||||
scopes: string[];
|
||||
|
||||
constructor(spreadsheetId: string, credentials: IGoogleAuthCredentials, options?: ISheetOptions | undefined) {
|
||||
// options = <SheetOptions>options || {};
|
||||
if (!options) {
|
||||
options = {} as ISheetOptions;
|
||||
}
|
||||
|
||||
this.id = spreadsheetId;
|
||||
this.credentials = credentials;
|
||||
this.scopes = options.scope || ['https://www.googleapis.com/auth/spreadsheets'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the cell values
|
||||
*/
|
||||
async getData(range: string): Promise<string[][] | undefined> {
|
||||
const client = await this.getAuthenticationClient();
|
||||
|
||||
const response = await Sheets.spreadsheets.values.get(
|
||||
{
|
||||
auth: client,
|
||||
spreadsheetId: this.id,
|
||||
range,
|
||||
}
|
||||
);
|
||||
|
||||
return response.data.values;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the cell values
|
||||
*/
|
||||
async batchUpdate(updateData: ISheetUpdateData[]) {
|
||||
const client = await this.getAuthenticationClient();
|
||||
|
||||
const response = await Sheets.spreadsheets.values.batchUpdate(
|
||||
{
|
||||
// @ts-ignore
|
||||
auth: client,
|
||||
spreadsheetId: this.id,
|
||||
valueInputOption: 'RAW',
|
||||
resource: {
|
||||
data: updateData,
|
||||
valueInputOption: "USER_ENTERED",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the cell values
|
||||
*/
|
||||
async setData(range: string, data: string[][]) {
|
||||
const client = await this.getAuthenticationClient();
|
||||
|
||||
const response = await Sheets.spreadsheets.values.update(
|
||||
{
|
||||
// @ts-ignore
|
||||
auth: client,
|
||||
spreadsheetId: this.id,
|
||||
range,
|
||||
valueInputOption: 'RAW',
|
||||
resource: {
|
||||
values: data
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends the cell values
|
||||
*/
|
||||
async appendData(range: string, data: string[][]) {
|
||||
const client = await this.getAuthenticationClient();
|
||||
|
||||
const response = await Sheets.spreadsheets.values.append(
|
||||
{
|
||||
// @ts-ignore
|
||||
auth: client,
|
||||
spreadsheetId: this.id,
|
||||
range,
|
||||
valueInputOption: 'RAW',
|
||||
resource: {
|
||||
values: data
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the authentication client needed to access spreadsheet
|
||||
*/
|
||||
async getAuthenticationClient(): Promise<JWT> {
|
||||
const client = new google.auth.JWT(
|
||||
this.credentials.email,
|
||||
undefined,
|
||||
this.credentials.privateKey,
|
||||
this.scopes,
|
||||
undefined
|
||||
);
|
||||
|
||||
// TODO: Check later if this or the above should be cached
|
||||
await client.authorize();
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the given sheet data in a strucutred way
|
||||
*/
|
||||
structureData(inputData: string[][], startRow: number, keys: string[]): IDataObject[] {
|
||||
const returnData = [];
|
||||
|
||||
let tempEntry: IDataObject, rowIndex: number, columnIndex: number, key: string;
|
||||
|
||||
for (rowIndex = startRow; rowIndex < inputData.length; rowIndex++) {
|
||||
tempEntry = {};
|
||||
for (columnIndex = 0; columnIndex < inputData[rowIndex].length; columnIndex++) {
|
||||
key = keys[columnIndex];
|
||||
if (key) {
|
||||
// Only add the data for which a key was given and ignore all others
|
||||
tempEntry[key] = inputData[rowIndex][columnIndex];
|
||||
}
|
||||
}
|
||||
if (Object.keys(tempEntry).length) {
|
||||
// Only add the entry if data got found to not have empty ones
|
||||
returnData.push(tempEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the given sheet data in a strucutred way using
|
||||
* the startRow as the one with the name of the key
|
||||
*/
|
||||
structureArrayDataByColumn(inputData: string[][], keyRow: number, dataStartRow: number): IDataObject[] {
|
||||
|
||||
const keys: string[] = [];
|
||||
|
||||
if (keyRow < 0 || dataStartRow < keyRow || keyRow >= inputData.length) {
|
||||
// The key row does not exist so it is not possible to strucutre data
|
||||
return [];
|
||||
}
|
||||
|
||||
// Create the keys array
|
||||
for (let columnIndex = 0; columnIndex < inputData[keyRow].length; columnIndex++) {
|
||||
keys.push(inputData[keyRow][columnIndex]);
|
||||
}
|
||||
|
||||
return this.structureData(inputData, dataStartRow, keys);
|
||||
}
|
||||
|
||||
|
||||
async appendSheetData(inputData: IDataObject[], range: string, keyRowIndex: number): Promise<string[][]> {
|
||||
const data = await this.convertStructuredDataToArray(inputData, range, keyRowIndex);
|
||||
return this.appendData(range, data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates data in a sheet
|
||||
*
|
||||
* @param {IDataObject[]} inputData Data to update Sheet with
|
||||
* @param {string} indexKey The name of the key which gets used to know which rows to update
|
||||
* @param {string} range The range to look for data
|
||||
* @param {number} keyRowIndex Index of the row which contains the keys
|
||||
* @param {number} dataStartRowIndex Index of the first row which contains data
|
||||
* @returns {Promise<string[][]>}
|
||||
* @memberof GoogleSheet
|
||||
*/
|
||||
async updateSheetData(inputData: IDataObject[], indexKey: string, range: string, keyRowIndex: number, dataStartRowIndex: number): Promise<string[][]> {
|
||||
// Get current data in Google Sheet
|
||||
let rangeStart: string, rangeEnd: string;
|
||||
let sheet: string | undefined = undefined;
|
||||
if (range.includes('!')) {
|
||||
[sheet, range] = range.split('!');
|
||||
}
|
||||
[rangeStart, rangeEnd] = range.split(':');
|
||||
|
||||
const keyRowRange = `${sheet ? sheet + '!' : ''}${rangeStart}${dataStartRowIndex}:${rangeEnd}${dataStartRowIndex}`;
|
||||
|
||||
const sheetDatakeyRow = await this.getData(keyRowRange);
|
||||
|
||||
if (sheetDatakeyRow === undefined) {
|
||||
throw new Error('Could not retrieve the key row!');
|
||||
}
|
||||
|
||||
const keyColumnOrder = sheetDatakeyRow[0];
|
||||
|
||||
const keyIndex = keyColumnOrder.indexOf(indexKey);
|
||||
|
||||
if (keyIndex === -1) {
|
||||
throw new Error(`Could not find column for key "${indexKey}"!`);
|
||||
}
|
||||
|
||||
const characterCode = rangeStart.toUpperCase().charCodeAt(0) + keyIndex;
|
||||
let keyColumnRange = String.fromCharCode(characterCode);
|
||||
keyColumnRange = `${sheet ? sheet + '!' : ''}${keyColumnRange}:${keyColumnRange}`;
|
||||
|
||||
const sheetDataKeyColumn = await this.getData(keyColumnRange);
|
||||
|
||||
if (sheetDataKeyColumn === undefined) {
|
||||
throw new Error('Could not retrieve the key column!');
|
||||
}
|
||||
|
||||
// TODO: The data till here can be cached optionally. Maybe add an option which can
|
||||
// can be activated if it is used in a loop and nothing else updates the data.
|
||||
|
||||
// Remove the first row which contains the key
|
||||
sheetDataKeyColumn.shift();
|
||||
|
||||
// Create an Array which all the key-values of the Google Sheet
|
||||
const keyColumnIndexLookup = sheetDataKeyColumn.map((rowContent) => rowContent[0] );
|
||||
|
||||
const updateData: ISheetUpdateData[] = [];
|
||||
let itemKey: string | number | undefined | null;
|
||||
let propertyName: string;
|
||||
let itemKeyIndex: number;
|
||||
let updateRowIndex: number;
|
||||
let updateColumnName: string;
|
||||
for (const inputItem of inputData) {
|
||||
itemKey = inputItem[indexKey] as string;
|
||||
// if ([undefined, null].includes(inputItem[indexKey] as string | undefined | null)) {
|
||||
if (itemKey === undefined || itemKey === null) {
|
||||
// Item does not have the indexKey so we can ignore it
|
||||
continue;
|
||||
}
|
||||
|
||||
// Item does have the key so check if it exists in Sheet
|
||||
itemKeyIndex = keyColumnIndexLookup.indexOf(itemKey as string);
|
||||
if (itemKeyIndex === -1) {
|
||||
// Key does not exist in the Sheet so it can not be updated so skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the row index in which the data should be updated
|
||||
// TODO: Should probably change the indexes to be 1 based because Google Sheet is
|
||||
updateRowIndex = keyColumnIndexLookup.indexOf(itemKey) + dataStartRowIndex + 1;
|
||||
|
||||
// Check all the properties in the sheet and check which ones exist on the
|
||||
// item and should be updated
|
||||
for (propertyName of keyColumnOrder) {
|
||||
if (propertyName === indexKey) {
|
||||
// Ignore the key itself as that does not get changed it gets
|
||||
// only used to find the correct row to update
|
||||
continue;
|
||||
}
|
||||
if (inputItem[propertyName] === undefined || inputItem[propertyName] === null) {
|
||||
// Property does not exist so skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
// Property exists so add it to the data to update
|
||||
|
||||
// Get the column name in which the property data can be found
|
||||
updateColumnName = String.fromCharCode(characterCode + keyColumnOrder.indexOf(propertyName));
|
||||
|
||||
updateData.push({
|
||||
range: `${sheet ? sheet + '!' : ''}${updateColumnName}${updateRowIndex}`,
|
||||
values: [
|
||||
[
|
||||
inputItem[propertyName] as string,
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return this.batchUpdate(updateData);
|
||||
}
|
||||
|
||||
|
||||
async convertStructuredDataToArray(inputData: IDataObject[], range: string, keyRowIndex: number): Promise<string[][]> {
|
||||
let startColumn, endColumn;
|
||||
let sheet: string | undefined = undefined;
|
||||
if (range.includes('!')) {
|
||||
[sheet, range] = range.split('!');
|
||||
}
|
||||
[startColumn, endColumn] = range.split(':');
|
||||
|
||||
|
||||
let getRange = `${startColumn}${keyRowIndex + 1}:${endColumn}${keyRowIndex + 1}`;
|
||||
|
||||
if (sheet !== undefined) {
|
||||
getRange = `${sheet}!${getRange}`;
|
||||
}
|
||||
|
||||
const keyColumnData = await this.getData(getRange);
|
||||
|
||||
if (keyColumnData === undefined) {
|
||||
throw new Error('Could not retrieve the column data!');
|
||||
}
|
||||
|
||||
const keyColumnOrder = keyColumnData[0];
|
||||
|
||||
const setData: string[][] = [];
|
||||
|
||||
let rowData: string[] = [];
|
||||
inputData.forEach((item) => {
|
||||
rowData = [];
|
||||
keyColumnOrder.forEach((key) => {
|
||||
if (item.hasOwnProperty(key) && item[key]) {
|
||||
rowData.push(item[key]!.toString());
|
||||
} else {
|
||||
rowData.push('');
|
||||
}
|
||||
});
|
||||
|
||||
setData.push(rowData);
|
||||
});
|
||||
|
||||
return setData;
|
||||
}
|
||||
|
||||
}
|
||||
1
packages/nodes-base/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './GoogleSheet';
|
||||
30
packages/nodes-base/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2017"
|
||||
],
|
||||
"types": [
|
||||
"node",
|
||||
"jest"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"removeComments": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"preserveConstEnums": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist/",
|
||||
"target": "es2017",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"credentials/**/*",
|
||||
"src/**/*",
|
||||
"nodes/**/*",
|
||||
"test/**/*",
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
103
packages/nodes-base/tslint.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"node_modules/**/*"
|
||||
]
|
||||
},
|
||||
"defaultSeverity": "error",
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"array-type": [
|
||||
true,
|
||||
"array-simple"
|
||||
],
|
||||
"arrow-return-shorthand": true,
|
||||
"ban": [
|
||||
true,
|
||||
{
|
||||
"name": "Array",
|
||||
"message": "tsstyle#array-constructor"
|
||||
}
|
||||
],
|
||||
"ban-types": [
|
||||
true,
|
||||
[
|
||||
"Object",
|
||||
"Use {} instead."
|
||||
],
|
||||
[
|
||||
"String",
|
||||
"Use 'string' instead."
|
||||
],
|
||||
[
|
||||
"Number",
|
||||
"Use 'number' instead."
|
||||
],
|
||||
[
|
||||
"Boolean",
|
||||
"Use 'boolean' instead."
|
||||
]
|
||||
],
|
||||
"class-name": true,
|
||||
"curly": [
|
||||
true,
|
||||
"ignore-same-line"
|
||||
],
|
||||
"forin": true,
|
||||
"jsdoc-format": true,
|
||||
"label-position": true,
|
||||
"member-access": [
|
||||
true,
|
||||
"no-public"
|
||||
],
|
||||
"new-parens": true,
|
||||
"no-angle-bracket-type-assertion": true,
|
||||
"no-any": true,
|
||||
"no-arg": true,
|
||||
"no-conditional-assignment": true,
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-default-export": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-inferrable-types": true,
|
||||
"no-namespace": [
|
||||
true,
|
||||
"allow-declarations"
|
||||
],
|
||||
"no-reference": true,
|
||||
"no-string-throw": true,
|
||||
"no-unused-expression": true,
|
||||
"no-var-keyword": true,
|
||||
"object-literal-shorthand": true,
|
||||
"only-arrow-functions": [
|
||||
true,
|
||||
"allow-declarations",
|
||||
"allow-named-functions"
|
||||
],
|
||||
"prefer-const": true,
|
||||
"radix": true,
|
||||
"semicolon": [
|
||||
true,
|
||||
"always",
|
||||
"ignore-bound-class-methods"
|
||||
],
|
||||
"switch-default": true,
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"use-isnan": true,
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"variable-name": [
|
||||
true,
|
||||
"check-format",
|
||||
"ban-keywords",
|
||||
"allow-leading-underscore",
|
||||
"allow-trailing-underscore"
|
||||
]
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
||||