init
This commit is contained in:
commit
bbfc60c096
18
Dockerfile
Executable file
18
Dockerfile
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
FROM n8nio/n8n:latest
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
nodejs \
|
||||||
|
npm
|
||||||
|
|
||||||
|
# Установка node-opcua
|
||||||
|
RUN npm install -g node-opcua
|
||||||
|
RUN npm install redis
|
||||||
|
RUN npm install node-cron
|
||||||
|
|
||||||
|
USER node
|
||||||
101
andrew_server.conf
Executable file
101
andrew_server.conf
Executable file
|
|
@ -0,0 +1,101 @@
|
||||||
|
client
|
||||||
|
dev tun
|
||||||
|
proto tcp
|
||||||
|
remote 185.185.120.73 443
|
||||||
|
resolv-retry infinite
|
||||||
|
nobind
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
|
||||||
|
;ca [inline]
|
||||||
|
;cert [inline]
|
||||||
|
;key [inline]
|
||||||
|
|
||||||
|
dhcp-option DNS 10.186.0.40
|
||||||
|
|
||||||
|
remote-cert-tls "server"
|
||||||
|
cipher AES-256-CBC
|
||||||
|
|
||||||
|
|
||||||
|
verb 3
|
||||||
|
|
||||||
|
;mute 20
|
||||||
|
|
||||||
|
#redirect-gateway def1
|
||||||
|
|
||||||
|
<ca>
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDRTCCAi2gAwIBAgIUVHCGM+p+xPzdpXv91y3tomQZlbEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFDESMBAGA1UEAwwJUHJvdGVpX1NNMB4XDTIyMDUxMjE1MTgxMVoXDTMyMDUw
|
||||||
|
OTE1MTgxMVowFDESMBAGA1UEAwwJUHJvdGVpX1NNMIIBIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAQ8AMIIBCgKCAQEAnXo9+W33IFWRyfNpw2+mKnuSJ+9Y/Y/ud7GMQCoihVQo
|
||||||
|
m5HOcp2GyT2KrnK3NgA/ZAcbWD6Nr5Zs2mqoaU6+BQdDW1NRYFpO55DCbcrBrxKR
|
||||||
|
mGXSzeAUI1739zdcHzhwOuNY1scOE80U5ZjTYus6raeXXXlEne7tDdzApFUF7HHj
|
||||||
|
KxrQI0R25Q2k9NetxInrsGu3O9meGbZb/dphsDFUJja86FxJ1gnuNpdIM5kya7b0
|
||||||
|
22CI44EMXYkvKZeAJmnuAPh3fah/t3xqYzNEp5PKXQE/CuwMH3Hf5JDSUMXHQt9p
|
||||||
|
z2BgmBX6O/5Yy1hx8ZO2lrRCM5RtU2QT2n0Tq1mnNwIDAQABo4GOMIGLMB0GA1Ud
|
||||||
|
DgQWBBTGZ+81Q0c58yv0Nykm/pmCfp6t0jBPBgNVHSMESDBGgBTGZ+81Q0c58yv0
|
||||||
|
Nykm/pmCfp6t0qEYpBYwFDESMBAGA1UEAwwJUHJvdGVpX1NNghRUcIYz6n7E/N2l
|
||||||
|
e/3XLe2iZBmVsTAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAQEAZXcDyMABGMYwF2IEA8zUcCjWwEG7e8y22VIltBkWCjqME9jy22MS
|
||||||
|
UZs8umumCoK/rMx7LwgiwKItBmi6N0pJf3yXL3tJlf73QGMRXyhkPxaCM4M5nceX
|
||||||
|
FBXiIKuXALT/jmCkF0g4fvJ70QU9tyW4wxcOpT808LHgk304yOKFF9Tny9kkpmHn
|
||||||
|
2KiqgvnYx5jY5bFanwZLOzGXBFo09vJ7JncaXccWxUIV1J8+QaAxF1rmd36+h7v0
|
||||||
|
8JIYUbop+8/BaXk8iN45xpklxvUA9LZbkRIqa9AlLyTxEzm5Ijy8u0lARgiDAQx1
|
||||||
|
49bubeRGGlnasxJ7T6CbC8ouscarE0TRAg==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
</ca>
|
||||||
|
|
||||||
|
<cert>
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDWDCCAkCgAwIBAgIRAK2JWMAqjyZjld5dOAYlg/0wDQYJKoZIhvcNAQELBQAw
|
||||||
|
FDESMBAGA1UEAwwJUHJvdGVpX1NNMB4XDTI0MDYwMzEzMjQyNVoXDTI3MDUxOTEz
|
||||||
|
MjQyNVowGDEWMBQGA1UEAwwNYW5kcmV3X3NlcnZlcjCCASIwDQYJKoZIhvcNAQEB
|
||||||
|
BQADggEPADCCAQoCggEBALeOQ1w3wzM6uIVvuWllPDxR4Rr5NMfE7TIxdLkZJF3t
|
||||||
|
Qg9j2HJNnNMAA3Q68f8T122Mq1rv7wOiJxdGYmkPUbbgeSGi2ZzOwJL0Ut1Glhjr
|
||||||
|
3YSXzDYPM+rNIILbgP4EaHq4SmkOrmup2Qq4cn5z5lY6E88Y1bVvq4aY035pzVGh
|
||||||
|
dukp8O9LE8mK/0LE/LdA+RMWEEfLE16serFOOCJxnuTPxgmbrYFuofRWEpbzLpIg
|
||||||
|
sn0FHdICKmX5YV1NxbIC14RlwDu90JYPR7W3LndyYhlFFHM/YyvK86f8D7K2AB41
|
||||||
|
hpFt1zt0zZOL1E12ION0KbMJHILg+qEWST8lDrd9dV0CAwEAAaOBoDCBnTAJBgNV
|
||||||
|
HRMEAjAAMB0GA1UdDgQWBBRNCfFYJty3vpA/EUPM2gCGaRE0YjBPBgNVHSMESDBG
|
||||||
|
gBTGZ+81Q0c58yv0Nykm/pmCfp6t0qEYpBYwFDESMBAGA1UEAwwJUHJvdGVpX1NN
|
||||||
|
ghRUcIYz6n7E/N2le/3XLe2iZBmVsTATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNV
|
||||||
|
HQ8EBAMCB4AwDQYJKoZIhvcNAQELBQADggEBAJJxBxZ63/1WL5VU8v9S8CecFyRx
|
||||||
|
waVFXEJ5i0XHKVFi3J8XjVWE2InnEaC75kXGnK3OKkXc1r1ZEdFT9OTdJ3qD6v3P
|
||||||
|
QdX6SQn0zKF5+SF9CSiPGzVofn+XyWkst/9KakXWslc8j+LbKM47SOZijyO3PcBr
|
||||||
|
bqemM5PEutpg5viUFhn93uzvhAQpsE476VCgn+EWXAu+siR9u+aQ+wG4vBM+HRmm
|
||||||
|
IIeKx4LKGFjPVBOtYrWpaVRGqs5LWFFqv1QXCGAouUXmVBMKOXddACvDpW0/WBaQ
|
||||||
|
1Rf+k0amDNBp1jL+fXoDP4URzE5OkahqQr+7xPV5b82loEEkrhnpAMHZfT8=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
</cert>
|
||||||
|
|
||||||
|
<key>
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3jkNcN8MzOriF
|
||||||
|
b7lpZTw8UeEa+TTHxO0yMXS5GSRd7UIPY9hyTZzTAAN0OvH/E9dtjKta7+8DoicX
|
||||||
|
RmJpD1G24HkhotmczsCS9FLdRpYY692El8w2DzPqzSCC24D+BGh6uEppDq5rqdkK
|
||||||
|
uHJ+c+ZWOhPPGNW1b6uGmNN+ac1RoXbpKfDvSxPJiv9CxPy3QPkTFhBHyxNerHqx
|
||||||
|
TjgicZ7kz8YJm62BbqH0VhKW8y6SILJ9BR3SAipl+WFdTcWyAteEZcA7vdCWD0e1
|
||||||
|
ty53cmIZRRRzP2MryvOn/A+ytgAeNYaRbdc7dM2Ti9RNdiDjdCmzCRyC4PqhFkk/
|
||||||
|
JQ63fXVdAgMBAAECggEBALV4GHBbUMidDUAerJfeVibsbDhWmC/IKRiufE9i4+lY
|
||||||
|
Xy8H7z1SLfEM2l1WyVj9LMZJhD9rZkmZTjxcgX6MfqTmV9tBPRMh9JFUq3fICSyq
|
||||||
|
Q16LFIF9lj5UfgfhKy32/bQV7rreoOIgEUvf/pG108r7sAnW67FsrY9sF+uqfips
|
||||||
|
YLE/UBQ9vXMoyfjJCzhRy53c7BNzG46ftVhwZzN7WtbT6EgYfyMZ4K0BpMOoy7JU
|
||||||
|
eSUX49cg6vAuwGAwx7s5bQYIoyyq8rd//ouKliltiycwPUQ8fM5TjkZQ+IEqaFGn
|
||||||
|
ZPY+yM17YLhZdPugoL5p6ZVYu3OLDfo2sPZQfboaXKECgYEA7kKyL8EuRTb4CaRO
|
||||||
|
953L9kzRZuNRBY0QTcXwSzTOb7fTBtFYITpwikLFvCWetMISnWXW7APcuro4MAP0
|
||||||
|
tVh27Gh+bu9ylY0X9M9phk004Yz17nLLhdzIQRr63zvXQE+kea0U1kbfoCR0JScn
|
||||||
|
CqsIg0w0pDLZmD1n8mc+Mcx1zJsCgYEAxTjhQnpUfrmeMSHY2E3obkcKXgkZTaFU
|
||||||
|
mO0dQVcTDfpSwhSTDBbZKJO/XVgVi5CFQ/3rv8OyTwO/Oxa2Pz3SLxH0vx4nAmHe
|
||||||
|
/iRxHYNEiGZlkrfvWyn2qv0n4hj6+FbNRUqee9Vudrhaa8Vn2dHW3gEqshu43KnF
|
||||||
|
Xplv3w6nGWcCgYBrzNl6KdmZj2liU2k3N7oU0mTcPHVoIY8C4U/7dwUAHxfnucg6
|
||||||
|
IIrSw6tbmVnZRwXNGUrItmakRaUudFu/wSrtg8nQv54EdDYmmcGQ9lW6x2BuZpoX
|
||||||
|
EvG4I1Dmt9iITICKXPlUifScLGIwfSp49TGA1nXq5Ob2rrHdR0Eonu2diQKBgF46
|
||||||
|
XuW0LMqfRMWUtlYiYvrNVla3Yi+E9TZbk660O5ZiE0hHLDqKcBbDAJtIioKz+fgS
|
||||||
|
SaDFj1vRHnzMzSZKEzIKEjV94oVw3WnvX1wKa6P/yGfhGask0YXAjl2oMcCVOF3y
|
||||||
|
2OWxg6WVTx5Oot+fMlm/lPaj6B8FvhcEmD6qcYvhAoGAQOZ5AlKDBTfYQleqc4gS
|
||||||
|
5Xtet7P0uQ3lRQ6DTuPHv8zaHYGnLlkRBDSVCN5rz2YU7frWKVvGq8S4w+0UTo0w
|
||||||
|
Q4npJ1RxCjMkQ2cHA5XNNWSyuV/gay+F8xmEbo815EjIXxzyxmx6OC+l6R/9uGhE
|
||||||
|
6fFpHGEJSzgZrGGv40b5Q18=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
</key>
|
||||||
20
custom_nodes_for_n8n-master/.editorconfig
Normal file
20
custom_nodes_for_n8n-master/.editorconfig
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[package.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
146
custom_nodes_for_n8n-master/.eslintrc.js
Normal file
146
custom_nodes_for_n8n-master/.eslintrc.js
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
/**
|
||||||
|
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
sourceType: 'module',
|
||||||
|
extraFileExtensions: ['.json'],
|
||||||
|
},
|
||||||
|
|
||||||
|
ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'],
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['package.json'],
|
||||||
|
plugins: ['eslint-plugin-n8n-nodes-base'],
|
||||||
|
extends: ['plugin:n8n-nodes-base/community'],
|
||||||
|
rules: {
|
||||||
|
'n8n-nodes-base/community-package-json-name-still-default': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['./credentials/**/*.ts'],
|
||||||
|
plugins: ['eslint-plugin-n8n-nodes-base'],
|
||||||
|
extends: ['plugin:n8n-nodes-base/credentials'],
|
||||||
|
rules: {
|
||||||
|
'n8n-nodes-base/cred-class-field-authenticate-type-assertion': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-field-display-name-missing-oauth2': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-field-display-name-miscased': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-field-documentation-url-missing': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off',
|
||||||
|
'n8n-nodes-base/cred-class-field-name-missing-oauth2': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-field-name-unsuffixed': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-field-name-uppercase-first-char': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-field-properties-assertion': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-field-type-options-password-missing': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-name-missing-oauth2-suffix': 'error',
|
||||||
|
'n8n-nodes-base/cred-class-name-unsuffixed': 'error',
|
||||||
|
'n8n-nodes-base/cred-filename-against-convention': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['./nodes/**/*.ts'],
|
||||||
|
plugins: ['eslint-plugin-n8n-nodes-base'],
|
||||||
|
extends: ['plugin:n8n-nodes-base/nodes'],
|
||||||
|
rules: {
|
||||||
|
'n8n-nodes-base/node-class-description-credentials-name-unsuffixed': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-display-name-unsuffixed-trigger-node': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-empty-string': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-icon-not-svg': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'off',
|
||||||
|
'n8n-nodes-base/node-class-description-inputs-wrong-trigger-node': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-missing-subtitle': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-non-core-color-present': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-name-miscased': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-name-unsuffixed-trigger-node': 'error',
|
||||||
|
'n8n-nodes-base/node-class-description-outputs-wrong': 'off',
|
||||||
|
'n8n-nodes-base/node-dirname-against-convention': 'error',
|
||||||
|
'n8n-nodes-base/node-execute-block-double-assertion-for-items': 'error',
|
||||||
|
'n8n-nodes-base/node-execute-block-wrong-error-thrown': 'error',
|
||||||
|
'n8n-nodes-base/node-filename-against-convention': 'error',
|
||||||
|
'n8n-nodes-base/node-param-array-type-assertion': 'error',
|
||||||
|
'n8n-nodes-base/node-param-color-type-unused': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-missing': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-wrong-for-boolean': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-wrong-for-collection': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-wrong-for-multi-options': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-wrong-for-number': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-wrong-for-simplify': 'error',
|
||||||
|
'n8n-nodes-base/node-param-default-wrong-for-string': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-boolean-without-whether': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-comma-separated-hyphen': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-empty-string': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-excess-final-period': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-excess-inner-whitespace': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-identical-to-display-name': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-line-break-html-tag': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-lowercase-first-char': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-miscased-id': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-miscased-json': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-miscased-url': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-missing-final-period': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-missing-for-return-all': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-missing-for-simplify': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-missing-from-dynamic-multi-options': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-missing-from-dynamic-options': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-missing-from-limit': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-unencoded-angle-brackets': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-unneeded-backticks': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-untrimmed': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-url-missing-protocol': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-weak': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-wrong-for-dynamic-options': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-wrong-for-ignore-ssl-issues': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-wrong-for-limit': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-wrong-for-return-all': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-wrong-for-simplify': 'error',
|
||||||
|
'n8n-nodes-base/node-param-description-wrong-for-upsert': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-excess-inner-whitespace': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-miscased-id': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-miscased': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-not-first-position': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-untrimmed': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-wrong-for-simplify': 'error',
|
||||||
|
'n8n-nodes-base/node-param-display-name-wrong-for-update-fields': 'error',
|
||||||
|
'n8n-nodes-base/node-param-min-value-wrong-for-limit': 'error',
|
||||||
|
'n8n-nodes-base/node-param-multi-options-type-unsorted-items': 'error',
|
||||||
|
'n8n-nodes-base/node-param-name-untrimmed': 'error',
|
||||||
|
'n8n-nodes-base/node-param-operation-option-action-wrong-for-get-many': 'error',
|
||||||
|
'n8n-nodes-base/node-param-operation-option-description-wrong-for-get-many': 'error',
|
||||||
|
'n8n-nodes-base/node-param-operation-option-without-action': 'error',
|
||||||
|
'n8n-nodes-base/node-param-operation-without-no-data-expression': 'error',
|
||||||
|
'n8n-nodes-base/node-param-option-description-identical-to-name': 'error',
|
||||||
|
'n8n-nodes-base/node-param-option-name-containing-star': 'error',
|
||||||
|
'n8n-nodes-base/node-param-option-name-duplicate': 'error',
|
||||||
|
'n8n-nodes-base/node-param-option-name-wrong-for-get-many': 'error',
|
||||||
|
'n8n-nodes-base/node-param-option-name-wrong-for-upsert': 'error',
|
||||||
|
'n8n-nodes-base/node-param-option-value-duplicate': 'error',
|
||||||
|
'n8n-nodes-base/node-param-options-type-unsorted-items': 'error',
|
||||||
|
'n8n-nodes-base/node-param-placeholder-miscased-id': 'error',
|
||||||
|
'n8n-nodes-base/node-param-placeholder-missing-email': 'error',
|
||||||
|
'n8n-nodes-base/node-param-required-false': 'error',
|
||||||
|
'n8n-nodes-base/node-param-resource-with-plural-option': 'error',
|
||||||
|
'n8n-nodes-base/node-param-resource-without-no-data-expression': 'error',
|
||||||
|
'n8n-nodes-base/node-param-type-options-missing-from-limit': 'error',
|
||||||
|
'n8n-nodes-base/node-param-type-options-password-missing': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
16
custom_nodes_for_n8n-master/.eslintrc.prepublish.js
Normal file
16
custom_nodes_for_n8n-master/.eslintrc.prepublish.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
extends: "./.eslintrc.js",
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['package.json'],
|
||||||
|
plugins: ['eslint-plugin-n8n-nodes-base'],
|
||||||
|
rules: {
|
||||||
|
'n8n-nodes-base/community-package-json-name-still-default': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
8
custom_nodes_for_n8n-master/.gitignore
vendored
Normal file
8
custom_nodes_for_n8n-master/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
.tmp
|
||||||
|
tmp
|
||||||
|
dist
|
||||||
|
npm-debug.log*
|
||||||
|
yarn.lock
|
||||||
|
.vscode/launch.json
|
||||||
2
custom_nodes_for_n8n-master/.npmignore
Normal file
2
custom_nodes_for_n8n-master/.npmignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
.DS_Store
|
||||||
|
*.tsbuildinfo
|
||||||
51
custom_nodes_for_n8n-master/.prettierrc.js
Normal file
51
custom_nodes_for_n8n-master/.prettierrc.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#semicolons
|
||||||
|
*/
|
||||||
|
semi: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#trailing-commas
|
||||||
|
*/
|
||||||
|
trailingComma: 'all',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#bracket-spacing
|
||||||
|
*/
|
||||||
|
bracketSpacing: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#tabs
|
||||||
|
*/
|
||||||
|
useTabs: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#tab-width
|
||||||
|
*/
|
||||||
|
tabWidth: 2,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#arrow-function-parentheses
|
||||||
|
*/
|
||||||
|
arrowParens: 'always',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#quotes
|
||||||
|
*/
|
||||||
|
singleQuote: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#quote-props
|
||||||
|
*/
|
||||||
|
quoteProps: 'as-needed',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#end-of-line
|
||||||
|
*/
|
||||||
|
endOfLine: 'lf',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://prettier.io/docs/en/options.html#print-width
|
||||||
|
*/
|
||||||
|
printWidth: 100,
|
||||||
|
};
|
||||||
7
custom_nodes_for_n8n-master/.vscode/extensions.json
vendored
Normal file
7
custom_nodes_for_n8n-master/.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
]
|
||||||
|
}
|
||||||
76
custom_nodes_for_n8n-master/CODE_OF_CONDUCT.md
Normal file
76
custom_nodes_for_n8n-master/CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at jan@n8n.io. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
19
custom_nodes_for_n8n-master/LICENSE.md
Normal file
19
custom_nodes_for_n8n-master/LICENSE.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright 2022 n8n
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
48
custom_nodes_for_n8n-master/README.md
Normal file
48
custom_nodes_for_n8n-master/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|

|
||||||
|
|
||||||
|
# n8n-nodes-starter
|
||||||
|
|
||||||
|
This repo contains example nodes to help you get started building your own custom integrations for [n8n](https://n8n.io). It includes the node linter and other dependencies.
|
||||||
|
|
||||||
|
To make your custom node available to the community, you must create it as an npm package, and [submit it to the npm registry](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry).
|
||||||
|
|
||||||
|
If you would like your node to be available on n8n cloud you can also [submit your node for verification](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
You need the following installed on your development machine:
|
||||||
|
|
||||||
|
* [git](https://git-scm.com/downloads)
|
||||||
|
* Node.js and npm. Minimum version Node 20. You can find instructions on how to install both using nvm (Node Version Manager) for Linux, Mac, and WSL [here](https://github.com/nvm-sh/nvm). For Windows users, refer to Microsoft's guide to [Install NodeJS on Windows](https://docs.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows).
|
||||||
|
* Install n8n with:
|
||||||
|
```
|
||||||
|
npm install n8n -g
|
||||||
|
```
|
||||||
|
* Recommended: follow n8n's guide to [set up your development environment](https://docs.n8n.io/integrations/creating-nodes/build/node-development-environment/).
|
||||||
|
|
||||||
|
## Using this starter
|
||||||
|
|
||||||
|
These are the basic steps for working with the starter. For detailed guidance on creating and publishing nodes, refer to the [documentation](https://docs.n8n.io/integrations/creating-nodes/).
|
||||||
|
|
||||||
|
1. [Generate a new repository](https://github.com/n8n-io/n8n-nodes-starter/generate) from this template repository.
|
||||||
|
2. Clone your new repo:
|
||||||
|
```
|
||||||
|
git clone https://github.com/<your organization>/<your-repo-name>.git
|
||||||
|
```
|
||||||
|
3. Run `npm i` to install dependencies.
|
||||||
|
4. Open the project in your editor.
|
||||||
|
5. Browse the examples in `/nodes` and `/credentials`. Modify the examples, or replace them with your own nodes.
|
||||||
|
6. Update the `package.json` to match your details.
|
||||||
|
7. Run `npm run lint` to check for errors or `npm run lintfix` to automatically fix errors when possible.
|
||||||
|
8. Test your node locally. Refer to [Run your node locally](https://docs.n8n.io/integrations/creating-nodes/test/run-node-locally/) for guidance.
|
||||||
|
9. Replace this README with documentation for your node. Use the [README_TEMPLATE](README_TEMPLATE.md) to get started.
|
||||||
|
10. Update the LICENSE file to use your details.
|
||||||
|
11. [Publish](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry) your package to npm.
|
||||||
|
|
||||||
|
## More information
|
||||||
|
|
||||||
|
Refer to our [documentation on creating nodes](https://docs.n8n.io/integrations/creating-nodes/) for detailed information on building your own nodes.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://github.com/n8n-io/n8n-nodes-starter/blob/master/LICENSE.md)
|
||||||
48
custom_nodes_for_n8n-master/README_TEMPLATE.md
Normal file
48
custom_nodes_for_n8n-master/README_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# n8n-nodes-_node-name_
|
||||||
|
|
||||||
|
This is an n8n community node. It lets you use _app/service name_ in your n8n workflows.
|
||||||
|
|
||||||
|
_App/service name_ is _one or two sentences describing the service this node integrates with_.
|
||||||
|
|
||||||
|
[n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform.
|
||||||
|
|
||||||
|
[Installation](#installation)
|
||||||
|
[Operations](#operations)
|
||||||
|
[Credentials](#credentials) <!-- delete if no auth needed -->
|
||||||
|
[Compatibility](#compatibility)
|
||||||
|
[Usage](#usage) <!-- delete if not using this section -->
|
||||||
|
[Resources](#resources)
|
||||||
|
[Version history](#version-history) <!-- delete if not using this section -->
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
_List the operations supported by your node._
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
_If users need to authenticate with the app/service, provide details here. You should include prerequisites (such as signing up with the service), available authentication methods, and how to set them up._
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
_State the minimum n8n version, as well as which versions you test against. You can also include any known version incompatibility issues._
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
_This is an optional section. Use it to help users with any difficult or confusing aspects of the node._
|
||||||
|
|
||||||
|
_By the time users are looking for community nodes, they probably already know n8n basics. But if you expect new users, you can link to the [Try it out](https://docs.n8n.io/try-it-out/) documentation to help them get started._
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
* [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes)
|
||||||
|
* _Link to app/service documentation._
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
_This is another optional section. If your node has multiple versions, include a short description of available versions and what changed, as well as any compatibility impact._
|
||||||
|
|
||||||
|
|
||||||
16
custom_nodes_for_n8n-master/gulpfile.js
Normal file
16
custom_nodes_for_n8n-master/gulpfile.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
const path = require('path');
|
||||||
|
const { task, src, dest } = require('gulp');
|
||||||
|
|
||||||
|
task('build:icons', copyIcons);
|
||||||
|
|
||||||
|
function copyIcons() {
|
||||||
|
const nodeSource = path.resolve('nodes', '**', '*.{png,svg}');
|
||||||
|
const nodeDestination = path.resolve('dist', 'nodes');
|
||||||
|
|
||||||
|
src(nodeSource).pipe(dest(nodeDestination));
|
||||||
|
|
||||||
|
const credSource = path.resolve('credentials', '**', '*.{png,svg}');
|
||||||
|
const credDestination = path.resolve('dist', 'credentials');
|
||||||
|
|
||||||
|
return src(credSource).pipe(dest(credDestination));
|
||||||
|
}
|
||||||
2
custom_nodes_for_n8n-master/index.d.ts
vendored
Normal file
2
custom_nodes_for_n8n-master/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './nodes/MyCustomNode/MyCustomNode.node';
|
||||||
|
export * from './nodes/Generator/Generator.node';
|
||||||
17
custom_nodes_for_n8n-master/index.js
Normal file
17
custom_nodes_for_n8n-master/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||||
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
__exportStar(require("./nodes/MyCustomNode/MyCustomNode"), exports);
|
||||||
2
custom_nodes_for_n8n-master/index.ts
Normal file
2
custom_nodes_for_n8n-master/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './nodes/MyCustomNode/MyCustomNode.node';
|
||||||
|
export * from './nodes/Generator/Generator.node';
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class _BaseNodeTransform implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'BaseNodeTransform',
|
||||||
|
name: 'baseNodeTransform',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Базовый узел transform, который выводит только метаданные',
|
||||||
|
defaults: {
|
||||||
|
name: 'BaseNodeTransform',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
|
||||||
|
return [
|
||||||
|
items.map(() => ({
|
||||||
|
json: {
|
||||||
|
DATA: {},
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
ITriggerFunctions,
|
||||||
|
ITriggerResponse,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class _BaseNodeTrigger implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'BaseNodeTrigger',
|
||||||
|
name: 'baseNodeTrigger',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Шаблон узла триггера, который эмитит только метадату',
|
||||||
|
defaults: {
|
||||||
|
name: 'BaseNodeTrigger',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Интервал (мс)',
|
||||||
|
name: 'interval',
|
||||||
|
type: 'number',
|
||||||
|
default: 1000,
|
||||||
|
description: 'Интервал между генерациями события',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||||
|
const interval = this.getNodeParameter('interval', 0) as number;
|
||||||
|
|
||||||
|
const workflowName = this.getWorkflow?.()?.name || '';
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name as string;
|
||||||
|
|
||||||
|
const emitFn = () => {
|
||||||
|
const data: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
DATA: {},
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pairedItem: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emit([[data]]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const intervalId = setInterval(emitFn, interval);
|
||||||
|
|
||||||
|
return {
|
||||||
|
closeFunction: async () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
127
custom_nodes_for_n8n-master/nodes/_ChainOut/_ChainOut.node.ts
Normal file
127
custom_nodes_for_n8n-master/nodes/_ChainOut/_ChainOut.node.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeOperationError,
|
||||||
|
NodeConnectionType,
|
||||||
|
IDataObject,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
function isObject(obj: unknown): obj is Record<string, any> {
|
||||||
|
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _ChainOut implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'ChainOut',
|
||||||
|
name: 'chainOut',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Отправляет данные в указанный workflow по ID',
|
||||||
|
defaults: {
|
||||||
|
name: 'ChainOut',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Workflow ID Source',
|
||||||
|
name: 'workflowIdSource',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Параметр',
|
||||||
|
value: 'parameter',
|
||||||
|
description: 'Брать ID цепочки из параметра',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'METADATA.ChainOut.name',
|
||||||
|
value: 'metadata',
|
||||||
|
description: 'Брать ID цепочки из метаданных',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'parameter',
|
||||||
|
description: 'Откуда брать ID цепочки назначения',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Workflow ID',
|
||||||
|
name: 'workflowId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
workflowIdSource: ['parameter'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: 'ID workflow для запуска',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
if (!items || items.length === 0) return [[]];
|
||||||
|
|
||||||
|
const item = items[0];
|
||||||
|
let workflowId = '';
|
||||||
|
const workflowIdSource = this.getNodeParameter('workflowIdSource', 0) as string;
|
||||||
|
|
||||||
|
if (workflowIdSource === 'parameter') {
|
||||||
|
workflowId = this.getNodeParameter('workflowId', 0) as string;
|
||||||
|
} else {
|
||||||
|
const metadataRaw = item.json.METADATA;
|
||||||
|
if (!isObject(metadataRaw)) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'METADATA не является объектом');
|
||||||
|
}
|
||||||
|
const metadata = metadataRaw as Record<string, any>;
|
||||||
|
|
||||||
|
if (!isObject(metadata.ChainOut)) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'METADATA.ChainOut не является объектом');
|
||||||
|
}
|
||||||
|
const chainOut = metadata.ChainOut as Record<string, any>;
|
||||||
|
const name = chainOut.name;
|
||||||
|
if (typeof name !== 'string' || name.trim() === '') {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'Workflow ID не найден в METADATA.ChainOut.name');
|
||||||
|
}
|
||||||
|
workflowId = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workflowId) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'Workflow ID пустой');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем новую METADATA
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
|
||||||
|
item.json.METADATA = {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
} as IDataObject;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const executionResult = await this.executeWorkflow(
|
||||||
|
{ id: workflowId },
|
||||||
|
[item], // передаём массив с одним элементом
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
parentExecution: {
|
||||||
|
executionId: this.getWorkflowDataProxy(0).$execution.id,
|
||||||
|
workflowId: this.getWorkflowDataProxy(0).$workflow.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return executionResult.data as INodeExecutionData[][];
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new NodeOperationError(this.getNode(), `Ошибка запуска workflow: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
IDataObject,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
interface SwitchCondition {
|
||||||
|
field: string;
|
||||||
|
operation: '==' | '!=' | '>' | '<' | '>=' | '<=';
|
||||||
|
value: string;
|
||||||
|
valueType: 'string' | 'number' | 'boolean';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _CustomSwitchNode implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Custom Switch',
|
||||||
|
name: 'customSwitchNode',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Узел Switch с несколькими условиями для поля внутри DATA',
|
||||||
|
defaults: { name: 'CustomSwitch' },
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main, NodeConnectionType.Main, NodeConnectionType.Main, NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Условия',
|
||||||
|
name: 'conditions',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
placeholder: 'Добавить условие',
|
||||||
|
typeOptions: { multipleValues: true },
|
||||||
|
default: [],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'condition',
|
||||||
|
displayName: 'Условие',
|
||||||
|
values: [
|
||||||
|
{ displayName: 'Поле (внутри DATA)', name: 'field', type: 'string', default: '' },
|
||||||
|
{
|
||||||
|
displayName: 'Операция', name: 'operation', type: 'options', options: [
|
||||||
|
{ name: '==', value: '==' },
|
||||||
|
{ name: '!=', value: '!=' },
|
||||||
|
{ name: '>', value: '>' },
|
||||||
|
{ name: '<', value: '<' },
|
||||||
|
{ name: '>=', value: '>=' },
|
||||||
|
{ name: '<=', value: '<=' },
|
||||||
|
], default: '=='
|
||||||
|
},
|
||||||
|
{ displayName: 'Значение', name: 'value', type: 'string', default: '' },
|
||||||
|
{
|
||||||
|
displayName: 'Тип значения', name: 'valueType', type: 'options', options: [
|
||||||
|
{ name: 'Строка', value: 'string' },
|
||||||
|
{ name: 'Число', value: 'number' },
|
||||||
|
{ name: 'Булево', value: 'boolean' },
|
||||||
|
], default: 'string'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
if (!items || items.length === 0) return [[]];
|
||||||
|
|
||||||
|
const item = items[0];
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
|
||||||
|
// Перезаписываем METADATA
|
||||||
|
item.json.METADATA = {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
} as IDataObject;
|
||||||
|
|
||||||
|
const conditions: SwitchCondition[] = this.getNodeParameter('conditions.condition', 0, []) as SwitchCondition[];
|
||||||
|
const returnData: INodeExecutionData[][] = [[], [], [], []]; // 4 выхода
|
||||||
|
|
||||||
|
const dataObj = item.json.DATA || {};
|
||||||
|
let routed = false;
|
||||||
|
|
||||||
|
for (let j = 0; j < conditions.length; j++) {
|
||||||
|
const { field, operation, value, valueType } = conditions[j];
|
||||||
|
const dataObjSafe = item.json.DATA as Record<string, any>;
|
||||||
|
let itemValue = dataObjSafe[field];
|
||||||
|
|
||||||
|
let compareValue: any = value;
|
||||||
|
if (valueType === 'number') compareValue = parseFloat(value);
|
||||||
|
else if (valueType === 'boolean') compareValue = value === 'true';
|
||||||
|
|
||||||
|
let match = false;
|
||||||
|
switch (operation) {
|
||||||
|
case '==': match = itemValue === compareValue; break;
|
||||||
|
case '!=': match = itemValue !== compareValue; break;
|
||||||
|
case '>': match = itemValue > compareValue; break;
|
||||||
|
case '<': match = itemValue < compareValue; break;
|
||||||
|
case '>=': match = itemValue >= compareValue; break;
|
||||||
|
case '<=': match = itemValue <= compareValue; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
returnData[j].push(item);
|
||||||
|
routed = true;
|
||||||
|
break; // первый совпавший выход
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если ни одно условие не совпало — направляем на последний выход
|
||||||
|
if (!routed && returnData.length > conditions.length) {
|
||||||
|
returnData[conditions.length].push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
}
|
||||||
162
custom_nodes_for_n8n-master/nodes/_ForLoop/_ForLoop.node.ts
Normal file
162
custom_nodes_for_n8n-master/nodes/_ForLoop/_ForLoop.node.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeOperationError,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
function checkCondition(value: any, cond: string, cmp: any): boolean {
|
||||||
|
switch (cond) {
|
||||||
|
case '!=': return value != cmp;
|
||||||
|
case '==': return value == cmp;
|
||||||
|
case '>': return value > cmp;
|
||||||
|
case '<': return value < cmp;
|
||||||
|
case '>=': return value >= cmp;
|
||||||
|
case '<=': return value <= cmp;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _ForLoop implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'ForLoop',
|
||||||
|
name: 'forLoop',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Цикл for с условием по полю JSON',
|
||||||
|
defaults: { name: 'ForLoop' },
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Поле (без DATA)',
|
||||||
|
name: 'fieldName',
|
||||||
|
type: 'string',
|
||||||
|
default: 'count',
|
||||||
|
placeholder: 'count',
|
||||||
|
description: 'Имя поля в item.json.DATA, по которому проверяется условие',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Условие',
|
||||||
|
name: 'condition',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: '!=', value: '!=' },
|
||||||
|
{ name: '==', value: '==' },
|
||||||
|
{ name: '>', value: '>' },
|
||||||
|
{ name: '<', value: '<' },
|
||||||
|
{ name: '>=', value: '>=' },
|
||||||
|
{ name: '<=', value: '<=' },
|
||||||
|
],
|
||||||
|
default: '!=',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Значение для сравнения',
|
||||||
|
name: 'compareValue',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Тип значения',
|
||||||
|
name: 'valueType',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'Number', value: 'number' },
|
||||||
|
{ name: 'String', value: 'string' },
|
||||||
|
],
|
||||||
|
default: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Начало',
|
||||||
|
name: 'start',
|
||||||
|
type: 'number',
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Конец',
|
||||||
|
name: 'end',
|
||||||
|
type: 'number',
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Шаг',
|
||||||
|
name: 'step',
|
||||||
|
type: 'number',
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Таймаут (мс)',
|
||||||
|
name: 'timeoutMs',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const item = this.getInputData()[0]; // всегда один вход
|
||||||
|
const returnItems: INodeExecutionData[] = [];
|
||||||
|
|
||||||
|
const fieldName = this.getNodeParameter('fieldName', 0) as string;
|
||||||
|
const condition = this.getNodeParameter('condition', 0) as string;
|
||||||
|
const compareValue = this.getNodeParameter('compareValue', 0) as number;
|
||||||
|
const valueType = this.getNodeParameter('valueType', 0) as string;
|
||||||
|
const start = this.getNodeParameter('start', 0) as number;
|
||||||
|
const end = this.getNodeParameter('end', 0) as number;
|
||||||
|
const step = this.getNodeParameter('step', 0) as number;
|
||||||
|
const timeoutMs = this.getNodeParameter('timeoutMs', 0) as number;
|
||||||
|
|
||||||
|
if (step === 0) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'Шаг не может быть равен нулю.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = item.json.DATA as Record<string, any>;
|
||||||
|
if (!data || data[fieldName] === undefined) {
|
||||||
|
throw new NodeOperationError(this.getNode(), `В item.json.DATA нет поля "${fieldName}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fieldValue = data[fieldName];
|
||||||
|
if (valueType === 'number') fieldValue = Number(fieldValue);
|
||||||
|
else if (valueType === 'string') fieldValue = String(fieldValue);
|
||||||
|
|
||||||
|
if (!checkCondition(fieldValue, condition, compareValue)) {
|
||||||
|
return this.prepareOutputData([]); // условие не прошло — пустой результат
|
||||||
|
}
|
||||||
|
|
||||||
|
const loopCondition = (val: number) => step > 0 ? val <= end : val >= end;
|
||||||
|
|
||||||
|
// Получаем данные для метадаты
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
|
||||||
|
for (let counter = start; loopCondition(counter); counter += step) {
|
||||||
|
const newItem: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
...item.json,
|
||||||
|
counter,
|
||||||
|
fieldValue,
|
||||||
|
METADATA: {
|
||||||
|
...(item.json.METADATA as Record<string, any> || {}),
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
binary: item.binary,
|
||||||
|
};
|
||||||
|
returnItems.push(newItem);
|
||||||
|
|
||||||
|
if (timeoutMs > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prepareOutputData(returnItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
ITriggerFunctions,
|
||||||
|
ITriggerResponse,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class _Generator implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Generator',
|
||||||
|
name: 'generatorNode',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Срабатывает по интервалу и генерирует JSON + metadata',
|
||||||
|
defaults: {
|
||||||
|
name: 'Generator',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Повторять раз в (мс)',
|
||||||
|
name: 'interval',
|
||||||
|
type: 'number',
|
||||||
|
default: 1000,
|
||||||
|
description: 'Интервал между генерацией данных',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'JSON на выходе',
|
||||||
|
name: 'jsonOutput',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: { rows: 6 },
|
||||||
|
default: '{}',
|
||||||
|
description: 'Содержимое JSON, которое будет выдано',
|
||||||
|
},
|
||||||
|
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||||
|
const interval = this.getNodeParameter('interval', 0) as number;
|
||||||
|
const jsonOutputStr = this.getNodeParameter('jsonOutput', 0) as string;
|
||||||
|
|
||||||
|
let parsedJson: any;
|
||||||
|
try {
|
||||||
|
parsedJson = JSON.parse(jsonOutputStr);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Ошибка при разборе JSON: ' + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const workflowName = this.getWorkflow?.()?.name || '';
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
|
||||||
|
const idField = nodeInfo.name as string;
|
||||||
|
|
||||||
|
const emitFn = () => {
|
||||||
|
const data: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
DATA:{
|
||||||
|
...parsedJson,
|
||||||
|
},
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pairedItem: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emit([[data]]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const intervalId = setInterval(emitFn, interval);
|
||||||
|
|
||||||
|
return {
|
||||||
|
closeFunction: async () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
112
custom_nodes_for_n8n-master/nodes/_HTTPIn/_HTTPIn.node.ts
Normal file
112
custom_nodes_for_n8n-master/nodes/_HTTPIn/_HTTPIn.node.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import {
|
||||||
|
IWebhookFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class _HTTPIn implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'HTTPInput',
|
||||||
|
name: 'httpInput',
|
||||||
|
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Receive HTTP requests like DemoHttpTransport',
|
||||||
|
defaults: {
|
||||||
|
name: 'HTTPInput',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(): Promise<INodeExecutionData[][]> {
|
||||||
|
return [[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
async webhook(this: IWebhookFunctions): Promise<{ workflowData: INodeExecutionData[][]; rawResponse: boolean }> {
|
||||||
|
const req = this.getRequestObject();
|
||||||
|
const res = this.getResponseObject();
|
||||||
|
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', '*');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.end();
|
||||||
|
return { workflowData: [[]], rawResponse: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = req.query || {};
|
||||||
|
const chain = query.chain as string;
|
||||||
|
|
||||||
|
if (!chain) {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.end("Error. Require param 'chain=chainName'");
|
||||||
|
return { workflowData: [[]], rawResponse: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: any = null;
|
||||||
|
try {
|
||||||
|
const contentType = req.headers['content-type'] || '';
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
body = await new Promise((resolve, reject) => {
|
||||||
|
let data = '';
|
||||||
|
req.on('data', (chunk: any) => { data += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.on('error', (err: any) => reject(err));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
body = await new Promise<string>((resolve) => {
|
||||||
|
let data = '';
|
||||||
|
req.on('data', (chunk: any) => { data += chunk; });
|
||||||
|
req.on('end', () => resolve(data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.end('Invalid body: ' + (e as Error).message);
|
||||||
|
return { workflowData: [[]], rawResponse: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
...(body && body.METADATA ? body.METADATA : {}), // если тело уже имеет METADATA
|
||||||
|
from: nodeInfo.name, // имя текущего узла
|
||||||
|
id: nodeInfo.name, // можно использовать nodeInfo.type для типа
|
||||||
|
chain: chain,
|
||||||
|
workflow: workflowName,
|
||||||
|
time: Date.now(),
|
||||||
|
date: new Date().toLocaleDateString('ru-RU'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const item: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
METADATA: metadata,
|
||||||
|
DATA: body,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.end('OK');
|
||||||
|
|
||||||
|
return {
|
||||||
|
workflowData: [[item]],
|
||||||
|
rawResponse: true, // мы сами ответили клиенту
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
149
custom_nodes_for_n8n-master/nodes/_MQTTInput/_MQTTInput.node.ts
Normal file
149
custom_nodes_for_n8n-master/nodes/_MQTTInput/_MQTTInput.node.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
ITriggerFunctions,
|
||||||
|
ITriggerResponse,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
|
// Функция замены $var на process.env.VAR
|
||||||
|
function resolveEnvVariables(str: string): string {
|
||||||
|
if (typeof str !== 'string') return str;
|
||||||
|
return str.replace(/\$([a-zA-Z0-9_]+)/g, (_, v) => process.env[v] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _MQTTInput implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'MQTTInput',
|
||||||
|
name: 'mqttInput',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Subscribe to MQTT topics and emit incoming messages',
|
||||||
|
defaults: {
|
||||||
|
name: 'MQTTInput',
|
||||||
|
},
|
||||||
|
inputs: [], // триггер — без входа
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Host URL',
|
||||||
|
name: 'host',
|
||||||
|
type: 'string',
|
||||||
|
default: 'tcp://$mqtthost',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Port',
|
||||||
|
name: 'port',
|
||||||
|
type: 'string',
|
||||||
|
default: '$mqttport',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Username',
|
||||||
|
name: 'username',
|
||||||
|
type: 'string',
|
||||||
|
default: '$mqttuser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Password',
|
||||||
|
name: 'password',
|
||||||
|
type: 'string',
|
||||||
|
|
||||||
|
default: '$mqttpassword',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Topic',
|
||||||
|
name: 'topic',
|
||||||
|
type: 'string',
|
||||||
|
default: '$mqtt_topic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'QoS',
|
||||||
|
name: 'qos',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 2,
|
||||||
|
},
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||||
|
const rawHost = this.getNodeParameter('host', 0) as string;
|
||||||
|
const rawPort = this.getNodeParameter('port', 0) as string;
|
||||||
|
const rawUsername = this.getNodeParameter('username', 0) as string;
|
||||||
|
const rawPassword = this.getNodeParameter('password', 0) as string;
|
||||||
|
const rawTopic = this.getNodeParameter('topic', 0) as string;
|
||||||
|
|
||||||
|
const qosRaw = this.getNodeParameter('qos', 0) as number;
|
||||||
|
const qos = (qosRaw === 0 || qosRaw === 1 || qosRaw === 2) ? qosRaw : 1;
|
||||||
|
|
||||||
|
// Подставляем переменные окружения
|
||||||
|
const host = resolveEnvVariables(rawHost);
|
||||||
|
const port = resolveEnvVariables(rawPort);
|
||||||
|
const username = resolveEnvVariables(rawUsername);
|
||||||
|
const password = resolveEnvVariables(rawPassword);
|
||||||
|
const topic = resolveEnvVariables(rawTopic);
|
||||||
|
|
||||||
|
const url = `${host}:${port}`;
|
||||||
|
|
||||||
|
const workflowName = this.getWorkflow()?.name || '';
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name as string;
|
||||||
|
|
||||||
|
const client = mqtt.connect(url, {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
client.subscribe(topic, { qos }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
this.emitError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', (receivedTopic, message) => {
|
||||||
|
const payloadStr = message.toString();
|
||||||
|
let payload: any;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(payloadStr);
|
||||||
|
} catch {
|
||||||
|
payload = { raw: payloadStr };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
DATA: payload,
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
mqtt_topic: receivedTopic,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pairedItem: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emit([[data]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err) => {
|
||||||
|
this.emitError(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
closeFunction: async () => {
|
||||||
|
client.end();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
141
custom_nodes_for_n8n-master/nodes/_MQTTOut/_MQTTOut.node.ts
Normal file
141
custom_nodes_for_n8n-master/nodes/_MQTTOut/_MQTTOut.node.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
|
function resolveEnvVariables(str: string): string {
|
||||||
|
return str.replace(/\$([a-zA-Z0-9_]+)/g, (_, v) => process.env[v] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _MQTTOut implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'MQTTOut',
|
||||||
|
name: 'mqttOut',
|
||||||
|
group: ['output'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Publish messages to MQTT broker',
|
||||||
|
defaults: {
|
||||||
|
name: 'MQTTOut',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
credentials: [],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Host URL',
|
||||||
|
name: 'host',
|
||||||
|
type: 'string',
|
||||||
|
default: 'tcp://$mqtthost',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Port',
|
||||||
|
name: 'port',
|
||||||
|
type: 'string',
|
||||||
|
default: '$mqttport',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Username',
|
||||||
|
name: 'username',
|
||||||
|
type: 'string',
|
||||||
|
default: '$mqttuser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Password',
|
||||||
|
name: 'password',
|
||||||
|
type: 'string',
|
||||||
|
default: '$mqttpassword',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'QoS',
|
||||||
|
name: 'qos',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: { minValue: 0, maxValue: 2 },
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Retain',
|
||||||
|
name: 'retain',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Topic',
|
||||||
|
name: 'topic',
|
||||||
|
type: 'string',
|
||||||
|
default: '101_AAA_0.12_UserToLocation_Out',
|
||||||
|
displayOptions: {
|
||||||
|
show: { useMetadataTopic: [false] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Use topic from METADATA',
|
||||||
|
name: 'useMetadataTopic',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions) {
|
||||||
|
const item = this.getInputData()[0]; // всегда один элемент
|
||||||
|
|
||||||
|
// Формируем METADATA заново
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
|
||||||
|
item.json.METADATA = {
|
||||||
|
from: nodeInfo.name,
|
||||||
|
id: nodeInfo.name,
|
||||||
|
workflow: workflowName,
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получаем параметры
|
||||||
|
const rawHost = this.getNodeParameter('host', 0) as string;
|
||||||
|
const rawPort = this.getNodeParameter('port', 0) as string;
|
||||||
|
const rawUsername = this.getNodeParameter('username', 0) as string;
|
||||||
|
const rawPassword = this.getNodeParameter('password', 0) as string;
|
||||||
|
|
||||||
|
const host = resolveEnvVariables(rawHost);
|
||||||
|
const port = resolveEnvVariables(rawPort);
|
||||||
|
const username = resolveEnvVariables(rawUsername);
|
||||||
|
const password = resolveEnvVariables(rawPassword);
|
||||||
|
|
||||||
|
const qosRaw = this.getNodeParameter('qos', 0) as number;
|
||||||
|
const qos = (qosRaw === 0 || qosRaw === 1 || qosRaw === 2) ? qosRaw : 1;
|
||||||
|
|
||||||
|
const retain = this.getNodeParameter('retain', 0) as boolean;
|
||||||
|
const useMetadataTopic = this.getNodeParameter('useMetadataTopic', 0) as boolean;
|
||||||
|
|
||||||
|
const url = `${host}:${port}`;
|
||||||
|
|
||||||
|
const metadata = item.json?.METADATA as { mqtt_topic?: string };
|
||||||
|
const topic = useMetadataTopic
|
||||||
|
? metadata?.mqtt_topic ?? 'undefined_topic'
|
||||||
|
: (this.getNodeParameter('topic', 0) as string);
|
||||||
|
|
||||||
|
const payload = JSON.stringify(item.json.DATA);
|
||||||
|
|
||||||
|
const client = mqtt.connect(url, { username, password });
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
client.on('connect', () => {
|
||||||
|
client.publish(topic, payload, { qos, retain }, (err) => {
|
||||||
|
client.end();
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.prepareOutputData([item]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
INodeExecutionData,
|
||||||
|
IExecuteFunctions,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class _MyCustomNode implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'My Custom Node',
|
||||||
|
name: 'myCustomNode',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Пример кастомного узла',
|
||||||
|
defaults: {
|
||||||
|
name: 'My Custom Node',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Text to Append',
|
||||||
|
name: 'appendText',
|
||||||
|
type: 'string',
|
||||||
|
default: ' Hello n8n!',
|
||||||
|
description: 'Текст, который будет добавлен к входу',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
const returnData: INodeExecutionData[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const appendText = this.getNodeParameter('appendText', i, '') as string;
|
||||||
|
const input = items[i].json['input'] as string || '';
|
||||||
|
|
||||||
|
const newItem: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
...items[i].json,
|
||||||
|
result: input + appendText,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
returnData.push(newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [returnData];
|
||||||
|
}
|
||||||
|
}
|
||||||
274
custom_nodes_for_n8n-master/nodes/_OPCInput/_OPCInput.node.ts
Normal file
274
custom_nodes_for_n8n-master/nodes/_OPCInput/_OPCInput.node.ts
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
import {
|
||||||
|
ITriggerFunctions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
NodeConnectionType
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ClientSession,
|
||||||
|
MessageSecurityMode,
|
||||||
|
SecurityPolicy,
|
||||||
|
OPCUAClient,
|
||||||
|
AttributeIds,
|
||||||
|
UserIdentityInfoUserName,
|
||||||
|
UserTokenType,
|
||||||
|
} from 'node-opcua';
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
function resolveEnvVariables(str: string): string {
|
||||||
|
if (typeof str !== 'string') return str;
|
||||||
|
return str.replace(/\$([a-zA-Z0-9_]+)/g, (_, v) => process.env[v] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpcInputOptions {
|
||||||
|
host: string;
|
||||||
|
port: string;
|
||||||
|
user?: string;
|
||||||
|
password?: string;
|
||||||
|
cert?: string;
|
||||||
|
key?: string;
|
||||||
|
sec_mode: string;
|
||||||
|
securityPolicyUri?: string;
|
||||||
|
applicationUri?: string;
|
||||||
|
NodeIdPath?: string;
|
||||||
|
ns: number;
|
||||||
|
interval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _OPCInput implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'OPC Input Trigger',
|
||||||
|
name: 'opcInput',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Connect to OPC UA server and periodically fetch values',
|
||||||
|
defaults: { name: 'OPC Input' },
|
||||||
|
inputs: [],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{ displayName: 'Host', name: 'host', type: 'string', default: '' },
|
||||||
|
{ displayName: 'Port', name: 'port', type: 'string', default: '4840' },
|
||||||
|
{ displayName: 'Username', name: 'user', type: 'string', default: '' },
|
||||||
|
{ displayName: 'Password', name: 'password', type: 'string', typeOptions: { password: true }, default: '' },
|
||||||
|
{ displayName: 'Certificate File Path', name: 'cert', type: 'string', default: '' },
|
||||||
|
{ displayName: 'Private Key File Path', name: 'key', type: 'string', default: '' },
|
||||||
|
{
|
||||||
|
displayName: 'Security Mode',
|
||||||
|
name: 'sec_mode',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'NONE', value: 'NONE' },
|
||||||
|
{ name: 'SIGN', value: 'SIGN' },
|
||||||
|
{ name: 'SIGNANDENCRYPT', value: 'SIGNANDENCRYPT' },
|
||||||
|
],
|
||||||
|
default: 'NONE',
|
||||||
|
},
|
||||||
|
{ displayName: 'Security Policy URI', name: 'securityPolicyUri', type: 'string', default: '' },
|
||||||
|
{ displayName: 'Application URI', name: 'applicationUri', type: 'string', default: '' },
|
||||||
|
{ displayName: 'NodeId Path', name: 'NodeIdPath', type: 'string', default: '' },
|
||||||
|
{ displayName: 'Namespace Index', name: 'ns', type: 'number', default: 1 },
|
||||||
|
{ displayName: 'Interval (ms)', name: 'interval', type: 'number', default: 5000 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async trigger(this: ITriggerFunctions) {
|
||||||
|
const options: OpcInputOptions = {
|
||||||
|
host: resolveEnvVariables((this.getNodeParameter('host', '') as string) || process.env.OPC_HOST || 'localhost'),
|
||||||
|
port: resolveEnvVariables((this.getNodeParameter('port', '4840') as string) || process.env.OPC_PORT || '4840'),
|
||||||
|
user: resolveEnvVariables((this.getNodeParameter('user', '') as string) || process.env.OPC_USER || ''),
|
||||||
|
password: resolveEnvVariables((this.getNodeParameter('password', '') as string) || process.env.OPC_PASS || ''),
|
||||||
|
cert: resolveEnvVariables((this.getNodeParameter('cert', '') as string) || process.env.OPC_CERT || ''),
|
||||||
|
key: resolveEnvVariables((this.getNodeParameter('key', '') as string) || process.env.OPC_KEY || ''),
|
||||||
|
sec_mode: resolveEnvVariables((this.getNodeParameter('sec_mode', 'NONE') as string) || process.env.OPC_SEC_MODE || 'NONE'),
|
||||||
|
securityPolicyUri: resolveEnvVariables((this.getNodeParameter('securityPolicyUri', '') as string) || process.env.OPC_SEC_POLICY || ''),
|
||||||
|
applicationUri: resolveEnvVariables((this.getNodeParameter('applicationUri', '') as string) || process.env.OPC_APP_URI || ''),
|
||||||
|
NodeIdPath: resolveEnvVariables((this.getNodeParameter('NodeIdPath', '') as string) || process.env.OPC_NODE_PATH || ''),
|
||||||
|
ns: (this.getNodeParameter('ns', 1) as number) || Number(process.env.OPC_NS) || 1,
|
||||||
|
interval: (this.getNodeParameter('interval', 5000) as number) || Number(process.env.OPC_INTERVAL) || 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpointUrl = `opc.tcp://${options.host}:${options.port}`;
|
||||||
|
|
||||||
|
let securityMode: MessageSecurityMode;
|
||||||
|
switch (options.sec_mode) {
|
||||||
|
case 'SIGN':
|
||||||
|
securityMode = MessageSecurityMode.Sign;
|
||||||
|
break;
|
||||||
|
case 'SIGNANDENCRYPT':
|
||||||
|
securityMode = MessageSecurityMode.SignAndEncrypt;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
securityMode = MessageSecurityMode.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let certificateFile: string | undefined;
|
||||||
|
let privateKeyFile: string | undefined;
|
||||||
|
|
||||||
|
if (process.env.OPC_CERT_BASE64) {
|
||||||
|
const certPath = path.join('/tmp', 'opc_cert.pem');
|
||||||
|
fs.writeFileSync(certPath, Buffer.from(process.env.OPC_CERT_BASE64, 'base64'));
|
||||||
|
certificateFile = certPath;
|
||||||
|
} else if (options.cert && fs.existsSync(options.cert)) {
|
||||||
|
certificateFile = options.cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.OPC_KEY_BASE64) {
|
||||||
|
const keyPath = path.join('/tmp', 'opc_key.pem');
|
||||||
|
fs.writeFileSync(keyPath, Buffer.from(process.env.OPC_KEY_BASE64, 'base64'));
|
||||||
|
privateKeyFile = keyPath;
|
||||||
|
} else if (options.key && fs.existsSync(options.key)) {
|
||||||
|
privateKeyFile = options.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = OPCUAClient.create({
|
||||||
|
securityMode,
|
||||||
|
securityPolicy: (options.securityPolicyUri as SecurityPolicy) || SecurityPolicy.None,
|
||||||
|
certificateFile,
|
||||||
|
privateKeyFile,
|
||||||
|
});
|
||||||
|
|
||||||
|
let connecting = false;
|
||||||
|
let disconnecting = false;
|
||||||
|
|
||||||
|
async function safeDisconnect() {
|
||||||
|
if (disconnecting) return;
|
||||||
|
disconnecting = true;
|
||||||
|
try { await client.disconnect(); } catch { }
|
||||||
|
disconnecting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рекурсивный обход объекта / папки
|
||||||
|
const browseAndRead = async (session: ClientSession, nodeId: string): Promise<any[]> => {
|
||||||
|
const browseResult = await session.browse(nodeId);
|
||||||
|
const results: any[] = [];
|
||||||
|
for (const ref of browseResult.references || []) {
|
||||||
|
if (ref.nodeClass === 2) { // Variable
|
||||||
|
try {
|
||||||
|
const dataValue = await session.read({ nodeId: ref.nodeId, attributeId: AttributeIds.Value });
|
||||||
|
results.push({
|
||||||
|
namespaceIndex: ref.nodeId.namespace,
|
||||||
|
browseName: ref.browseName.toString(),
|
||||||
|
displayName: ref.displayName.text,
|
||||||
|
identifier: ref.nodeId.value,
|
||||||
|
value: dataValue.value.value,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
results.push({
|
||||||
|
namespaceIndex: ref.nodeId.namespace,
|
||||||
|
browseName: ref.browseName.toString(),
|
||||||
|
displayName: ref.displayName.text,
|
||||||
|
identifier: ref.nodeId.value,
|
||||||
|
value: `Error: ${(e as Error).message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else { // Object / Folder
|
||||||
|
results.push({
|
||||||
|
namespaceIndex: ref.nodeId.namespace,
|
||||||
|
browseName: ref.browseName.toString(),
|
||||||
|
displayName: ref.displayName.text,
|
||||||
|
identifier: ref.nodeId.value,
|
||||||
|
value: await browseAndRead(session, ref.nodeId.toString()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitValues = async () => {
|
||||||
|
if (connecting) return;
|
||||||
|
connecting = true;
|
||||||
|
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect(endpointUrl);
|
||||||
|
|
||||||
|
const session = await client.createSession(
|
||||||
|
options.user
|
||||||
|
? {
|
||||||
|
type: UserTokenType.UserName,
|
||||||
|
userName: options.user,
|
||||||
|
password: options.password || "",
|
||||||
|
} as UserIdentityInfoUserName
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeId = `ns=${options.ns};s=${options.NodeIdPath}`;
|
||||||
|
let values: any[] = [];
|
||||||
|
|
||||||
|
// Пробуем прочитать как переменную
|
||||||
|
try {
|
||||||
|
const dataValue = await session.read({ nodeId, attributeId: AttributeIds.Value });
|
||||||
|
if (dataValue.value.value !== null && dataValue.value.value !== undefined) {
|
||||||
|
values = [dataValue.value.value];
|
||||||
|
} else {
|
||||||
|
// Если это объект / папка — рекурсивное чтение
|
||||||
|
values = await browseAndRead(session, nodeId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
values = await browseAndRead(session, nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = [
|
||||||
|
{
|
||||||
|
DATA: {
|
||||||
|
connection_status: true,
|
||||||
|
time: Date.now(),
|
||||||
|
values: [values],
|
||||||
|
},
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.emit([this.helpers.returnJsonArray(output)]);
|
||||||
|
|
||||||
|
await session.close();
|
||||||
|
await safeDisconnect();
|
||||||
|
} catch (err) {
|
||||||
|
const output = [
|
||||||
|
{
|
||||||
|
DATA: {
|
||||||
|
connection_status: false,
|
||||||
|
time: Date.now(),
|
||||||
|
values: [],
|
||||||
|
error: (err as Error).message,
|
||||||
|
},
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.emit([this.helpers.returnJsonArray(output)]);
|
||||||
|
await safeDisconnect();
|
||||||
|
} finally {
|
||||||
|
connecting = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const intervalHandle = setInterval(emitValues, options.interval);
|
||||||
|
|
||||||
|
async function closeFunction() {
|
||||||
|
clearInterval(intervalHandle);
|
||||||
|
await safeDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { closeFunction };
|
||||||
|
}
|
||||||
|
}
|
||||||
192
custom_nodes_for_n8n-master/nodes/_OPCServer/_OPCServer.node.ts
Normal file
192
custom_nodes_for_n8n-master/nodes/_OPCServer/_OPCServer.node.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { OPCUAClient, AttributeIds, DataType } from 'node-opcua';
|
||||||
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
|
function getValueByPath(obj: any, path: string): any {
|
||||||
|
if (!path) return obj;
|
||||||
|
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _OPCServer implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'OPCServer',
|
||||||
|
name: 'opcServer',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Записывает данные в OPC UA и, при необходимости, публикует в MQTT',
|
||||||
|
defaults: {
|
||||||
|
name: 'OPCServer',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Requests or Devices',
|
||||||
|
name: 'sectionDevices',
|
||||||
|
type: 'string',
|
||||||
|
default: 'Устройства',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Путь до данных внутри DATA',
|
||||||
|
name: 'useKeyFromData',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Имя сущности (например: Весы, Табло и т.д.)',
|
||||||
|
name: 'entityName',
|
||||||
|
type: 'string',
|
||||||
|
default: 'Состояние',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Тип сущности',
|
||||||
|
name: 'entityType',
|
||||||
|
type: 'string',
|
||||||
|
default: 'Весы',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Локация',
|
||||||
|
name: 'location',
|
||||||
|
type: 'string',
|
||||||
|
default: '9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Отправлять в MQTT',
|
||||||
|
name: 'sendToMqtt',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Сетевой узел',
|
||||||
|
name: 'networkNode',
|
||||||
|
type: 'string',
|
||||||
|
default: 'dev_re_promuc_local',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions) {
|
||||||
|
const items = this.getInputData();
|
||||||
|
const sectionDevices = this.getNodeParameter('sectionDevices', 0) as string;
|
||||||
|
const useKeyFromData = this.getNodeParameter('useKeyFromData', 0) as string;
|
||||||
|
const entityName = this.getNodeParameter('entityName', 0) as string;
|
||||||
|
const entityType = this.getNodeParameter('entityType', 0) as string;
|
||||||
|
const location = this.getNodeParameter('location', 0) as string;
|
||||||
|
const networkNode = this.getNodeParameter('networkNode', 0) as string;
|
||||||
|
const sendToMqtt = this.getNodeParameter('sendToMqtt', 0) as boolean;
|
||||||
|
|
||||||
|
const opcHost = process.env.OPChost;
|
||||||
|
const opcPort = process.env.OPCport;
|
||||||
|
|
||||||
|
if (!opcHost || !opcPort) {
|
||||||
|
throw new Error('OPChost или OPCport не заданы в переменных окружения');
|
||||||
|
}
|
||||||
|
|
||||||
|
const opcEndpoint = `opc.tcp://${opcHost}:${opcPort}`;
|
||||||
|
const client = OPCUAClient.create({ endpointMustExist: false });
|
||||||
|
|
||||||
|
await client.connect(opcEndpoint);
|
||||||
|
const session = await client.createSession();
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
const rawData = item.json.DATA as Record<string, any>;
|
||||||
|
const metadata = item.json.METADATA as { nodeId?: string };
|
||||||
|
|
||||||
|
if (!rawData) continue;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const dataToWrite = getValueByPath(rawData, useKeyFromData);
|
||||||
|
if (typeof dataToWrite !== 'object' || dataToWrite === null) {
|
||||||
|
throw new Error(`Не удалось получить данные по пути '${useKeyFromData}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let basePath: string;
|
||||||
|
if (typeof rawData.route === 'string') {
|
||||||
|
basePath = rawData.route;
|
||||||
|
} else {
|
||||||
|
basePath = `${networkNode}.${location}.${sectionDevices}.${entityType}.${entityName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opcNamespacePath = `${basePath}`;
|
||||||
|
Object.entries(dataToWrite).forEach(([key, value]) => {
|
||||||
|
console.log(`key=${key}, value=`, value, typeof value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodesToWrite = Object.entries(dataToWrite).map(([key, value]) => {
|
||||||
|
let dataType: DataType = DataType.String;
|
||||||
|
if (typeof value === 'boolean') dataType = DataType.Boolean;
|
||||||
|
else if (typeof value === 'number') dataType = DataType.Double;
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodeId: `ns=1;s=${opcNamespacePath}.${key}`,
|
||||||
|
attributeId: AttributeIds.Value,
|
||||||
|
value: {
|
||||||
|
value: {
|
||||||
|
dataType,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.write(nodesToWrite);
|
||||||
|
const writeResults = await session.write(nodesToWrite);
|
||||||
|
|
||||||
|
writeResults.forEach((statusCode, index) => {
|
||||||
|
const nodeId = nodesToWrite[index].nodeId;
|
||||||
|
console.log(`Write to ${nodeId} => status: ${statusCode.name || statusCode.toString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ===== MQTT (если включено) =====
|
||||||
|
if (sendToMqtt) {
|
||||||
|
const mqttHost = process.env.mqtthost;
|
||||||
|
const mqttPort = process.env.mqttport;
|
||||||
|
const mqttUser = process.env.mqttuser;
|
||||||
|
const mqttPass = process.env.mqttpassword;
|
||||||
|
|
||||||
|
if (!mqttHost || !mqttPort || !mqttUser || !mqttPass) {
|
||||||
|
throw new Error('Отсутствуют переменные окружения для MQTT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.stringify(dataToWrite);
|
||||||
|
const mqttUrl = `mqtt://${mqttHost}:${mqttPort}`;
|
||||||
|
const mqttClient = mqtt.connect(mqttUrl, {
|
||||||
|
username: mqttUser,
|
||||||
|
password: mqttPass,
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
mqttClient.on('connect', () => {
|
||||||
|
mqttClient.publish(basePath, payload, { qos: 1, retain: false }, (err) => {
|
||||||
|
mqttClient.end();
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mqttClient.on('error', (err) => reject(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof item.json.DATA === 'object' && item.json.DATA !== null) {
|
||||||
|
(item.json.DATA as Record<string, any>).route = opcNamespacePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await session.close();
|
||||||
|
await client.disconnect();
|
||||||
|
|
||||||
|
return this.prepareOutputData(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
299
custom_nodes_for_n8n-master/nodes/_OPCWrite/_OPCWrite.node.ts
Normal file
299
custom_nodes_for_n8n-master/nodes/_OPCWrite/_OPCWrite.node.ts
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeOperationError,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
// NOTE: We do NOT import IExecuteFunctions (you said it's not available).
|
||||||
|
// We'll use `this: any & OPCWrite` in execute.
|
||||||
|
|
||||||
|
import {
|
||||||
|
OPCUAClient,
|
||||||
|
ClientSession,
|
||||||
|
AttributeIds,
|
||||||
|
DataType,
|
||||||
|
StatusCodes,
|
||||||
|
// node-opcua typings provide these enums/constructs
|
||||||
|
} from 'node-opcua';
|
||||||
|
|
||||||
|
export class _OPCWrite implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'OPCWrite',
|
||||||
|
name: 'opcWrite',
|
||||||
|
icon: 'file:opc.png',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Writes data to an OPC UA server (behavior like C++ open62541 example)',
|
||||||
|
defaults: {
|
||||||
|
name: 'OPCWrite',
|
||||||
|
color: '#772244',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Host',
|
||||||
|
name: 'host',
|
||||||
|
type: 'string',
|
||||||
|
default: process.env.OPC_HOST || 'opc.tcp://localhost',
|
||||||
|
placeholder: 'opc.tcp://localhost',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Port',
|
||||||
|
name: 'port',
|
||||||
|
type: 'number',
|
||||||
|
default: Number(process.env.OPC_PORT) || 4840,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Node ID (identifier part or full ns=..;s=..)',
|
||||||
|
name: 'nodeId',
|
||||||
|
type: 'string',
|
||||||
|
default: process.env.OPC_NODE_ID || '',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Route (dot-path from DATA)',
|
||||||
|
name: 'route',
|
||||||
|
type: 'string',
|
||||||
|
default: 'value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Namespace index (used when nodeId is not full)',
|
||||||
|
name: 'ns',
|
||||||
|
type: 'number',
|
||||||
|
default: Number(process.env.OPC_NS) || 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Single client & session stored on the instance (like C++)
|
||||||
|
private client: OPCUAClient | null = null;
|
||||||
|
private session: ClientSession | null = null;
|
||||||
|
private endpointUrlCached: string | null = null;
|
||||||
|
private connectedOnce = false; // tracks whether we already tried initial connect
|
||||||
|
|
||||||
|
// Helper: replace ${ENV} in strings (like you asked earlier)
|
||||||
|
private replaceEnvVars(str: string): string {
|
||||||
|
if (!str) return str;
|
||||||
|
return str.replace(/\$\{([^}]+)\}/g, (_m, name) => process.env[name] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private splitRoute(route: string): string[] {
|
||||||
|
if (!route) return [];
|
||||||
|
if (route.startsWith('DATA.')) route = route.slice(5);
|
||||||
|
return route.length ? route.split('.') : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getValueByRoute(obj: any, steps: string[]): any {
|
||||||
|
let cur = obj;
|
||||||
|
for (const step of steps) {
|
||||||
|
if (cur === null || cur === undefined) {
|
||||||
|
throw new Error(`не удалось найти поле: '${step}' (промежуточное значение пустое)`);
|
||||||
|
}
|
||||||
|
if (Array.isArray(cur)) {
|
||||||
|
const idx = Number(step);
|
||||||
|
if (Number.isNaN(idx)) throw new Error(`не удалось обратиться к элементу массива с индексом: ${step}`);
|
||||||
|
if (idx < 0 || idx >= cur.length) throw new Error(`Индекс вне диапазона: ${step}`);
|
||||||
|
cur = cur[idx];
|
||||||
|
} else {
|
||||||
|
if (typeof cur !== 'object') throw new Error(`Поле ${step} не является объектом/массивом`);
|
||||||
|
if (!(step in cur)) throw new Error(`не удалось найти поле: '${step}'`);
|
||||||
|
cur = cur[step];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure client & session exist. To mimic C++ (which connects in ctor),
|
||||||
|
* we attempt to create client & connect on first call (synchronously awaited here).
|
||||||
|
*
|
||||||
|
* IMPORTANT: we do NOT implement robust reconnection here (only one attempt on BadSecureChannelClosed),
|
||||||
|
* to match the C++ behavior.
|
||||||
|
*/
|
||||||
|
private async ensureConnectedFirstTime(endpointUrl: string): Promise<void> {
|
||||||
|
if (this.client && this.session && this.endpointUrlCached === endpointUrl) {
|
||||||
|
this.connectedOnce = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If previously attempted and failed, we still try to create new client (like C++ does once)
|
||||||
|
try {
|
||||||
|
// create client and connect
|
||||||
|
this.client = OPCUAClient.create({ endpoint_must_exist: false });
|
||||||
|
this.endpointUrlCached = endpointUrl;
|
||||||
|
// await immediate connect (C++ does synchronous connect in ctor)
|
||||||
|
await this.client.connect(endpointUrl);
|
||||||
|
this.session = await this.client.createSession();
|
||||||
|
this.connectedOnce = true;
|
||||||
|
} catch (err) {
|
||||||
|
// In C++ you logged warning and carried on; do similar: keep client null but don't implement automatic retries
|
||||||
|
this.client = null;
|
||||||
|
this.session = null;
|
||||||
|
this.connectedOnce = false;
|
||||||
|
// throw to let node/user see connection error (C++ only warned; choose to reflect error)
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform write, but mimic C++ behavior:
|
||||||
|
* - Try writing once.
|
||||||
|
* - If failure and error equals BadSecureChannelClosed (or status code indicates that),
|
||||||
|
* attempt the write a second time WITHOUT recreating client/session (just retry).
|
||||||
|
* - If still fails — throw.
|
||||||
|
*/
|
||||||
|
private async writeLikeCpp(nodeId: string, value: any, dataType: DataType): Promise<void> {
|
||||||
|
if (!this.session) throw new Error('session not available');
|
||||||
|
const nodesToWrite = [
|
||||||
|
{
|
||||||
|
nodeId,
|
||||||
|
attributeId: AttributeIds.Value,
|
||||||
|
value: {
|
||||||
|
value: {
|
||||||
|
dataType,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// first attempt
|
||||||
|
try {
|
||||||
|
const res = await (this.session as any).write(nodesToWrite);
|
||||||
|
const status = Array.isArray(res) ? res[0] : res;
|
||||||
|
if (status && status !== StatusCodes.Good) {
|
||||||
|
// emulate C++: if BADSECURECHANNELCLOSED then try once more
|
||||||
|
if (status === StatusCodes.BadSecureChannelClosed) {
|
||||||
|
// second attempt — without recreating client/session (like your C++ code tried write again)
|
||||||
|
const res2 = await (this.session as any).write(nodesToWrite);
|
||||||
|
const status2 = Array.isArray(res2) ? res2[0] : res2;
|
||||||
|
if (status2 && status2 !== StatusCodes.Good) {
|
||||||
|
throw new Error(`OPC UA write error after retry: ${status2.toString ? status2.toString() : status2}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`OPC UA write error: ${status.toString ? status.toString() : status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} catch (err: any) {
|
||||||
|
// node-opcua may throw; try to inspect error.statusCode
|
||||||
|
// If it's the secure channel closed, mimic C++: one retry call (without reconnect).
|
||||||
|
const sc = err && err.statusCode ? err.statusCode : null;
|
||||||
|
if (sc === StatusCodes.BadSecureChannelClosed) {
|
||||||
|
try {
|
||||||
|
const res2 = await (this.session as any).write(nodesToWrite);
|
||||||
|
const status2 = Array.isArray(res2) ? res2[0] : res2;
|
||||||
|
if (status2 && status2 !== StatusCodes.Good) {
|
||||||
|
throw new Error(`OPC UA write error after retry: ${status2.toString ? status2.toString() : status2}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} catch (err2) {
|
||||||
|
throw err2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise rethrow original error
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute uses any for this to avoid requiring n8n-core types
|
||||||
|
async execute(this: any & _OPCWrite): Promise<INodeExecutionData[][]> {
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop() : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
|
||||||
|
const itemCount = this.getInputData().length;
|
||||||
|
|
||||||
|
// read params (allow ${ENV})
|
||||||
|
const rawHost = this.getNodeParameter('host', 0) as string;
|
||||||
|
const rawPort = this.getNodeParameter('port', 0) as number;
|
||||||
|
const rawNodeId = this.getNodeParameter('nodeId', 0) as string;
|
||||||
|
const rawRoute = this.getNodeParameter('route', 0) as string;
|
||||||
|
const ns = this.getNodeParameter('ns', 0) as number;
|
||||||
|
|
||||||
|
const host = this.replaceEnvVars(rawHost);
|
||||||
|
const port = rawPort;
|
||||||
|
const nodeIdParam = this.replaceEnvVars(rawNodeId);
|
||||||
|
const route = this.replaceEnvVars(rawRoute);
|
||||||
|
|
||||||
|
// normalize endpoint url
|
||||||
|
let endpointUrl = host;
|
||||||
|
if (!/^opc.tcp:\/\//.test(endpointUrl)) endpointUrl = `opc.tcp://${endpointUrl}`;
|
||||||
|
if (!/:\d+$/.test(endpointUrl) && port) endpointUrl = `${endpointUrl}:${port}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.connectedOnce) {
|
||||||
|
await this.ensureConnectedFirstTime(endpointUrl);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new NodeOperationError(this.getNode(), `Не удалось подключиться к OPC UA: ${(err as Error).message || err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputs: INodeExecutionData[] = [];
|
||||||
|
const routeSteps = this.splitRoute(route);
|
||||||
|
|
||||||
|
for (let i = 0; i < itemCount; i++) {
|
||||||
|
const item = this.getInputData()[i];
|
||||||
|
const dataRoot = item.json['DATA'];
|
||||||
|
if (dataRoot === undefined) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'Входной объект не содержит поля DATA');
|
||||||
|
}
|
||||||
|
|
||||||
|
let val: any;
|
||||||
|
try {
|
||||||
|
val = routeSteps.length ? this.getValueByRoute(dataRoot, routeSteps) : dataRoot;
|
||||||
|
} catch (err) {
|
||||||
|
throw new NodeOperationError(this.getNode(), (err as Error).message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataType: DataType;
|
||||||
|
let valueToWrite: any = val;
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
dataType = DataType.String;
|
||||||
|
} else if (typeof val === 'boolean') {
|
||||||
|
dataType = DataType.Boolean;
|
||||||
|
} else if (typeof val === 'number') {
|
||||||
|
if (Number.isInteger(val)) {
|
||||||
|
if (val >= -32768 && val <= 32767) dataType = (DataType as any).Int16 ?? DataType.Int32;
|
||||||
|
else dataType = (DataType as any).Int32 ?? DataType.Int32;
|
||||||
|
} else {
|
||||||
|
dataType = DataType.Double;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'тип поля не является простым. Разрешены только Number, Boolean, String.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodeIdFull = nodeIdParam;
|
||||||
|
if (!/^ns=\d+;/.test(nodeIdFull)) {
|
||||||
|
nodeIdFull = `ns=${ns};s=${nodeIdFull}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.session) throw new Error('session not available');
|
||||||
|
await this.writeLikeCpp(nodeIdFull, valueToWrite, dataType);
|
||||||
|
} catch (err) {
|
||||||
|
throw new NodeOperationError(this.getNode(), `OPC UA write error: ${(err as Error).message || err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outJson: any = {
|
||||||
|
DATA: val,
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
outputs.push({ json: outJson });
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prepareOutputData(outputs);
|
||||||
|
}
|
||||||
|
}
|
||||||
125
custom_nodes_for_n8n-master/nodes/_ORMwrite/_ORMwrite.node.ts
Normal file
125
custom_nodes_for_n8n-master/nodes/_ORMwrite/_ORMwrite.node.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import {
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
NodeConnectionType,
|
||||||
|
IExecuteFunctions
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { Client } from 'pg';
|
||||||
|
|
||||||
|
// Замена $VAR на process.env.VAR
|
||||||
|
function resolveEnvVariables(str: string): string {
|
||||||
|
return str.replace(/\$([a-zA-Z0-9_]+)/g, (_, v) => process.env[v] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _ORMwrite implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'ORM Write',
|
||||||
|
name: 'ormWrite',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Writes data into PostgreSQL table (creates if not exists)',
|
||||||
|
defaults: {
|
||||||
|
name: 'ORM Write',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{ displayName: 'Host', name: 'host', type: 'string', default: '$dbhost', required: true },
|
||||||
|
{ displayName: 'Port', name: 'port', type: 'string', default: '$dbport' },
|
||||||
|
{ displayName: 'Database', name: 'database', type: 'string', default: '$dbname', required: true },
|
||||||
|
{ displayName: 'User', name: 'user', type: 'string', default: '$dbuser' },
|
||||||
|
{ displayName: 'Password', name: 'password', type: 'string', default: '$dbpassword' },
|
||||||
|
{ displayName: 'SSL', name: 'ssl', type: 'boolean', default: false },
|
||||||
|
{ displayName: 'Table Name', name: 'table', type: 'string', default: '', required: true },
|
||||||
|
{ displayName: 'Create Table If Not Exists', name: 'createIfNotExists', type: 'boolean', default: true },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const item = this.getInputData()[0]; // всегда один элемент
|
||||||
|
|
||||||
|
// Формируем METADATA заново
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
item.json.METADATA = {
|
||||||
|
from: nodeInfo.name,
|
||||||
|
id: nodeInfo.name,
|
||||||
|
workflow: workflowName,
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Параметры подключения
|
||||||
|
const host = resolveEnvVariables(this.getNodeParameter('host', 0) as string);
|
||||||
|
const port = parseInt(resolveEnvVariables(this.getNodeParameter('port', 0) as string), 10) || 5432;
|
||||||
|
const database = resolveEnvVariables(this.getNodeParameter('database', 0) as string);
|
||||||
|
const user = resolveEnvVariables(this.getNodeParameter('user', 0) as string);
|
||||||
|
const password = resolveEnvVariables(this.getNodeParameter('password', 0) as string);
|
||||||
|
const ssl = this.getNodeParameter('ssl', 0) as boolean;
|
||||||
|
const table = this.getNodeParameter('table', 0) as string;
|
||||||
|
const createIfNotExists = this.getNodeParameter('createIfNotExists', 0) as boolean;
|
||||||
|
|
||||||
|
const client = new Client({ host, port, database, user, password, ssl });
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Проверяем существование таблицы
|
||||||
|
const tableExistsRes = await client.query(
|
||||||
|
`SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name = $1
|
||||||
|
) AS exists`,
|
||||||
|
[table],
|
||||||
|
);
|
||||||
|
const tableExists = tableExistsRes.rows[0].exists;
|
||||||
|
|
||||||
|
const data = item.json.DATA as { [key: string]: any };
|
||||||
|
if (typeof data !== 'object' || data === null) {
|
||||||
|
await client.end();
|
||||||
|
throw new Error('DATA должно быть объектом в первом элементе');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём таблицу, если нужно
|
||||||
|
if (!tableExists && createIfNotExists) {
|
||||||
|
const columns = Object.entries(data)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
if (key === 'id') throw new Error("Column 'id' is reserved");
|
||||||
|
if (typeof value === 'number' && Number.isInteger(value)) return `"${key}" BIGINT`;
|
||||||
|
if (typeof value === 'number') return `"${key}" REAL`;
|
||||||
|
if (typeof value === 'boolean') return `"${key}" BOOLEAN`;
|
||||||
|
if (typeof value === 'string') return `"${key}" TEXT`;
|
||||||
|
return `"${key}" JSONB`;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
await client.query(`CREATE TABLE "${table}" (id SERIAL PRIMARY KEY, ${columns});`);
|
||||||
|
} else if (!tableExists && !createIfNotExists) {
|
||||||
|
await client.end();
|
||||||
|
throw new Error(`Table ${table} does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем список существующих столбцов
|
||||||
|
const colRes = await client.query(
|
||||||
|
`SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public' AND table_name = $1 AND column_name != 'id'`,
|
||||||
|
[table],
|
||||||
|
);
|
||||||
|
const columns = colRes.rows.map((r) => r.column_name);
|
||||||
|
|
||||||
|
const usedCols = columns.filter((c) => data[c] !== undefined);
|
||||||
|
if (usedCols.length === 0) {
|
||||||
|
await client.end();
|
||||||
|
throw new Error('Нет данных для вставки: ни один столбец не совпадает');
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = usedCols.map((c) => data[c]);
|
||||||
|
const placeholders = usedCols.map((_, i) => `$${i + 1}`).join(', ');
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO "${table}" (${usedCols.map((c) => `"${c}"`).join(', ')}) VALUES (${placeholders})`,
|
||||||
|
values,
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.end();
|
||||||
|
|
||||||
|
return this.prepareOutputData([item]);
|
||||||
|
}
|
||||||
|
}
|
||||||
150
custom_nodes_for_n8n-master/nodes/_Postgres/_Postgres.node.ts
Normal file
150
custom_nodes_for_n8n-master/nodes/_Postgres/_Postgres.node.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeOperationError,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { Client } from 'pg';
|
||||||
|
|
||||||
|
function isRecordWithPostgresQuery(obj: unknown): obj is Record<string, any> & { PostgresQuery: string } {
|
||||||
|
return (
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
obj !== null &&
|
||||||
|
!Array.isArray(obj) &&
|
||||||
|
typeof (obj as Record<string, any>).PostgresQuery === 'string' &&
|
||||||
|
(obj as Record<string, any>).PostgresQuery.trim() !== ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _Postgres implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Postgres',
|
||||||
|
name: 'postgres',
|
||||||
|
group: ['output'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Выполняет SQL запрос в Postgres из METADATA.PostgresQuery',
|
||||||
|
defaults: {
|
||||||
|
name: 'Postgres',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Host',
|
||||||
|
name: 'host',
|
||||||
|
type: 'string',
|
||||||
|
default: process.env.DB_HOST || '',
|
||||||
|
placeholder: 'Например, localhost',
|
||||||
|
description: 'Postgres host из переменной окружения DB_HOST',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Port',
|
||||||
|
name: 'port',
|
||||||
|
type: 'number',
|
||||||
|
default: Number(process.env.DB_PORT) || 5432,
|
||||||
|
description: 'Порт Postgres из переменной окружения DB_PORT',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Database',
|
||||||
|
name: 'database',
|
||||||
|
type: 'string',
|
||||||
|
default: process.env.DB_NAME || '',
|
||||||
|
description: 'Имя базы данных из переменной окружения DB_NAME',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User',
|
||||||
|
name: 'user',
|
||||||
|
type: 'string',
|
||||||
|
default: process.env.DB_USER || '',
|
||||||
|
description: 'Пользователь базы из переменной окружения DB_USER',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Password',
|
||||||
|
name: 'password',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
default: process.env.DB_PASSWORD || '',
|
||||||
|
description: 'Пароль из переменной окружения DB_PASSWORD',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Use SSL',
|
||||||
|
name: 'ssl',
|
||||||
|
type: 'boolean',
|
||||||
|
default: process.env.DB_SSL === 'true' || false,
|
||||||
|
description: 'Использовать SSL-соединение',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'Нет входящих данных');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем/дополняем METADATA для всех items
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
for (const item of items) {
|
||||||
|
item.json.METADATA = {
|
||||||
|
...(item.json.METADATA as Record<string, any> || {}),
|
||||||
|
from: nodeInfo.name,
|
||||||
|
id: nodeInfo.name,
|
||||||
|
workflow: workflowName,
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstItem = items[0];
|
||||||
|
const metadata = firstItem.json.METADATA;
|
||||||
|
|
||||||
|
if (!isRecordWithPostgresQuery(metadata)) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'METADATA.PostgresQuery не найден или не является строкой');
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = metadata.PostgresQuery;
|
||||||
|
|
||||||
|
// Параметры подключения
|
||||||
|
const host = this.getNodeParameter('host', 0) as string;
|
||||||
|
const port = this.getNodeParameter('port', 0) as number;
|
||||||
|
const database = this.getNodeParameter('database', 0) as string;
|
||||||
|
const user = this.getNodeParameter('user', 0) as string;
|
||||||
|
const password = this.getNodeParameter('password', 0) as string;
|
||||||
|
const ssl = this.getNodeParameter('ssl', 0) as boolean;
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
database,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
ssl: ssl ? { rejectUnauthorized: false } : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const res = await client.query(query);
|
||||||
|
|
||||||
|
await client.end();
|
||||||
|
|
||||||
|
return this.prepareOutputData(res.rows);
|
||||||
|
} catch (error: any) {
|
||||||
|
try {
|
||||||
|
await client.end();
|
||||||
|
} catch { }
|
||||||
|
throw new NodeOperationError(this.getNode(), `Ошибка выполнения запроса: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
IHttpRequestMethods,
|
||||||
|
IDataObject,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class _RestOutput implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'RestOutput',
|
||||||
|
name: 'restOutput',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Выполняет HTTP-запрос по URI из METADATA или из поля URI',
|
||||||
|
defaults: {
|
||||||
|
name: 'RestOutput',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Динамическая настройка',
|
||||||
|
name: 'useDynamicUri',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: 'Брать URI из METADATA или использовать указанный ниже',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'URI',
|
||||||
|
name: 'staticUri',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
useDynamicUri: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Метод',
|
||||||
|
name: 'method',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'GET', value: 'GET' },
|
||||||
|
{ name: 'POST', value: 'POST' },
|
||||||
|
{ name: 'PUT', value: 'PUT' },
|
||||||
|
{ name: 'DELETE', value: 'DELETE' },
|
||||||
|
],
|
||||||
|
default: 'GET',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Headers',
|
||||||
|
name: 'headers',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: { multipleValues: true },
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'header',
|
||||||
|
displayName: 'Header',
|
||||||
|
values: [
|
||||||
|
{ name: 'key', displayName: 'Key', type: 'string', default: '' },
|
||||||
|
{ name: 'value', displayName: 'Value', type: 'string', default: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const item = this.getInputData()[0]; // один элемент
|
||||||
|
const useDynamicUri = this.getNodeParameter('useDynamicUri', 0) as boolean;
|
||||||
|
const staticUri = useDynamicUri ? undefined : this.getNodeParameter('staticUri', 0) as string;
|
||||||
|
const method = this.getNodeParameter('method', 0) as IHttpRequestMethods;
|
||||||
|
const headersInput = this.getNodeParameter('headers', 0, []) as { header: Array<{ key: string; value: string }> };
|
||||||
|
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNode = nodeInfo.type.split('.').pop()!;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
|
||||||
|
const headerObj: Record<string, string> = {};
|
||||||
|
for (const header of headersInput.header || []) {
|
||||||
|
if (header.key) {
|
||||||
|
headerObj[header.key] = header.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomMetadata extends IDataObject {
|
||||||
|
HTTPoutput?: {
|
||||||
|
uri?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = item.json?.METADATA as CustomMetadata | undefined;
|
||||||
|
const uri = useDynamicUri ? metadata?.HTTPoutput?.uri : staticUri;
|
||||||
|
|
||||||
|
if (!uri) {
|
||||||
|
throw new Error('URI не задан ни в METADATA, ни вручную');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.helpers.httpRequest({
|
||||||
|
method,
|
||||||
|
url: uri,
|
||||||
|
headers: headerObj,
|
||||||
|
json: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [[
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
DATA: {
|
||||||
|
body: response,
|
||||||
|
headers: [],
|
||||||
|
status_code: 200,
|
||||||
|
},
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
ITriggerFunctions,
|
||||||
|
ITriggerResponse,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import cron from 'node-cron';
|
||||||
|
|
||||||
|
export class _Schedule implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Scheduler',
|
||||||
|
name: 'schedulerNode',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Срабатывает каждый день в заданное время и генерирует JSON + metadata',
|
||||||
|
defaults: {
|
||||||
|
name: 'Scheduler',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Часы (0–23)',
|
||||||
|
name: 'hour',
|
||||||
|
type: 'number',
|
||||||
|
default: 9,
|
||||||
|
typeOptions: { minValue: 0, maxValue: 23 },
|
||||||
|
description: 'Время запуска — часы (по серверу)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Минуты (0–59)',
|
||||||
|
name: 'minute',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
typeOptions: { minValue: 0, maxValue: 59 },
|
||||||
|
description: 'Время запуска — минуты (по серверу)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'JSON на выходе',
|
||||||
|
name: 'jsonOutput',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: { rows: 6 },
|
||||||
|
default: '{}',
|
||||||
|
description: 'Содержимое JSON, которое будет выдано при срабатывании',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
|
||||||
|
const hour = this.getNodeParameter('hour', 0) as number;
|
||||||
|
const minute = this.getNodeParameter('minute', 0) as number;
|
||||||
|
const jsonOutputStr = this.getNodeParameter('jsonOutput', 0) as string;
|
||||||
|
|
||||||
|
let parsedJson: any;
|
||||||
|
try {
|
||||||
|
parsedJson = JSON.parse(jsonOutputStr);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Ошибка при разборе JSON: ' + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowName = this.getWorkflow().name || '';
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name as string;
|
||||||
|
|
||||||
|
const emitFn = () => {
|
||||||
|
const data: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
DATA: { ...parsedJson },
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pairedItem: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emit([[data]]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Генерируем cron-строку: "минуты часы * * *"
|
||||||
|
const cronExpression = `${minute} ${hour} * * *`;
|
||||||
|
|
||||||
|
const task = cron.schedule(cronExpression, emitFn);
|
||||||
|
|
||||||
|
return {
|
||||||
|
closeFunction: async () => {
|
||||||
|
task.stop();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class _ScriptFilter implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'ScriptFilter',
|
||||||
|
name: 'scriptFilter',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Выполняет скрипт на JavaScript над item',
|
||||||
|
defaults: {
|
||||||
|
name: 'ScriptFilter',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Код скрипта',
|
||||||
|
name: 'code',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
rows: 30,
|
||||||
|
},
|
||||||
|
default: 'obj = item.json;\nreturn obj;',
|
||||||
|
description: 'JavaScript-код, который будет выполнен над элементом',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const item = this.getInputData()[0]; // берём один элемент
|
||||||
|
const code = this.getNodeParameter('code', 0) as string;
|
||||||
|
|
||||||
|
// Создаем функцию вида: function(item) { ...код... }
|
||||||
|
const userFunction = new Function('item', code);
|
||||||
|
|
||||||
|
// Формируем METADATA заново
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
|
||||||
|
item.json.METADATA = {
|
||||||
|
from: nodeInfo.name, // текущий узел
|
||||||
|
id: nodeInfo.name, // идентификатор узла
|
||||||
|
workflow: workflowName, // имя workflow
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let outputData: INodeExecutionData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const output = userFunction.call(null, item);
|
||||||
|
outputData = {
|
||||||
|
json: output.json ?? output,
|
||||||
|
binary: output.binary ?? undefined,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Ошибка выполнения скрипта: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [[outputData]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import * as redis from 'redis';
|
||||||
|
|
||||||
|
export class _StorageReader implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'StorageReader',
|
||||||
|
name: 'storageReader',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Получает из Redis ключи Storage и добавляет их в DATA, остальные данные передаёт дальше',
|
||||||
|
defaults: {
|
||||||
|
name: 'StorageReader',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Redis Storage Keys',
|
||||||
|
name: 'redisStorageKeys',
|
||||||
|
type: 'string',
|
||||||
|
default: 'started_sessions,shift_restart',
|
||||||
|
description: 'Через запятую перечислите ключи из Redis для получения Storage',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const item = this.getInputData()[0]; // Берём один элемент
|
||||||
|
|
||||||
|
// Получаем ключи из параметров узла
|
||||||
|
const redisStorageKeysStr = this.getNodeParameter('redisStorageKeys', 0) as string;
|
||||||
|
const redisStorageKeys = redisStorageKeysStr.split(',').map(k => k.trim()).filter(k => k.length > 0);
|
||||||
|
|
||||||
|
// Берём хост и порт из переменных окружения
|
||||||
|
const redisHost = process.env.REDIS_HOST || 'redis';
|
||||||
|
const redisPort = Number(process.env.REDIS_PORT || '6379');
|
||||||
|
|
||||||
|
const client = redis.createClient({
|
||||||
|
socket: { host: redisHost, port: redisPort },
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err: Error) => this.logger.error('Redis Client Error', { message: err.message, stack: err.stack }));
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const storageData: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const key of redisStorageKeys) {
|
||||||
|
try {
|
||||||
|
const rawValue = await client.get(key);
|
||||||
|
if (rawValue) {
|
||||||
|
try {
|
||||||
|
storageData[key] = JSON.parse(rawValue);
|
||||||
|
} catch {
|
||||||
|
storageData[key] = rawValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
storageData[key] = null;
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
this.logger.warn(`Ошибка при получении ключа "${key}" из Redis: ${(e as Error).message || e}`);
|
||||||
|
storageData[key] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.quit();
|
||||||
|
|
||||||
|
// Формируем METADATA заново
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
|
||||||
|
const newItem: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
DATA: {
|
||||||
|
...(item.json.DATA && typeof item.json.DATA === 'object' ? item.json.DATA : {}),
|
||||||
|
Storage: storageData,
|
||||||
|
},
|
||||||
|
METADATA: {
|
||||||
|
from: nodeInfo.name,
|
||||||
|
id: nodeInfo.name,
|
||||||
|
workflow: workflowName,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
binary: item.binary,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [[newItem]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import * as redis from 'redis';
|
||||||
|
|
||||||
|
function getValueByPath(obj: any, path: string): any {
|
||||||
|
return path.split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _StorageWriter implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'StorageWriter',
|
||||||
|
name: 'storageWriter',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Сохраняет данные в Redis по ключу',
|
||||||
|
defaults: {
|
||||||
|
name: 'StorageWriter',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Redis Key',
|
||||||
|
name: 'redisKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'Ключ в Redis, под которым сохраняется значение',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Path to Value (например, DATA.Storage.shift_restart)',
|
||||||
|
name: 'valuePath',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: false,
|
||||||
|
description: 'Путь к значению внутри JSON. Если пусто — сохраняется весь DATA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const item = this.getInputData()[0]; // Берём один элемент
|
||||||
|
const json = item.json;
|
||||||
|
|
||||||
|
const redisHost = process.env.REDIS_HOST || 'redis';
|
||||||
|
const redisPort = Number(process.env.REDIS_PORT || '6379');
|
||||||
|
|
||||||
|
const client = redis.createClient({
|
||||||
|
socket: { host: redisHost, port: redisPort },
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err: Error) =>
|
||||||
|
this.logger.error('Redis Client Error', { message: err.message, stack: err.stack }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Формируем METADATA заново
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
|
||||||
|
json.METADATA = {
|
||||||
|
from: nodeInfo.name,
|
||||||
|
id: nodeInfo.name,
|
||||||
|
workflow: workflowName,
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = this.getNodeParameter('redisKey', 0, '') as string;
|
||||||
|
const valuePath = this.getNodeParameter('valuePath', 0, '') as string;
|
||||||
|
|
||||||
|
const valueToStore = valuePath ? getValueByPath(json, valuePath) : json.DATA;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.set(key, JSON.stringify(valueToStore));
|
||||||
|
this.logger.info(`Сохранено в Redis: [${key}] =`, valueToStore);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
this.logger.error(`Ошибка записи ключа "${key}" в Redis`, { error: e });
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.quit();
|
||||||
|
|
||||||
|
return [[item]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
INodeExecutionData,
|
||||||
|
IExecuteFunctions,
|
||||||
|
NodeConnectionType,
|
||||||
|
IDataObject
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class _Summator implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Summator',
|
||||||
|
name: 'summator',
|
||||||
|
icon: 'fa:random',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Aggregates DATA from multiple inputs into one object',
|
||||||
|
defaults: {
|
||||||
|
name: 'Summator',
|
||||||
|
color: '#772244',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
const workflowName = this.getWorkflow().name || '';
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name as string;
|
||||||
|
|
||||||
|
const parsedJson: Record<string, IDataObject> = {};
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const json = item.json as any; // входные данные лежат в item.json
|
||||||
|
if (!json || !json.DATA || !json.METADATA || !json.METADATA.from) continue;
|
||||||
|
|
||||||
|
const key = json.METADATA.from as string;
|
||||||
|
parsedJson[key] = json.DATA as IDataObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: INodeExecutionData = {
|
||||||
|
json: {
|
||||||
|
DATA: parsedJson,
|
||||||
|
METADATA: {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [[result]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { IExecuteFunctions } from 'n8n-workflow';
|
||||||
|
import { INodeType, INodeTypeDescription, NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
export class _TimeConverter implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Time Converter',
|
||||||
|
name: 'timeConverter',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Convert between Unix timestamp and formatted time',
|
||||||
|
defaults: {
|
||||||
|
name: 'Time Converter',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Source Field',
|
||||||
|
name: 'source',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Path to the field containing the time value (dot notation supported)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Destination Field',
|
||||||
|
name: 'dest',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Path to save the converted value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Date/Time Format',
|
||||||
|
name: 'dateTimeFormat',
|
||||||
|
type: 'string',
|
||||||
|
default: 'yyyy-MM-dd HH:mm:ss',
|
||||||
|
description: 'Luxon format string (use yyyy, MM, dd, HH, mm, ss, SSS for ms)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Mode',
|
||||||
|
name: 'mode',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'Unix Time → String', value: 'unixtime' },
|
||||||
|
{ name: 'String → Unix Time', value: 'time' },
|
||||||
|
],
|
||||||
|
default: 'unixtime',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions) {
|
||||||
|
const item = this.getInputData()[0]; // один элемент
|
||||||
|
if (!item) return [[]];
|
||||||
|
|
||||||
|
const source = this.getNodeParameter('source', 0) as string;
|
||||||
|
const dest = this.getNodeParameter('dest', 0) as string;
|
||||||
|
let dateTimeFormat = this.getNodeParameter('dateTimeFormat', 0) as string;
|
||||||
|
const mode = this.getNodeParameter('mode', 0) as string;
|
||||||
|
|
||||||
|
if (dateTimeFormat.includes('%ms')) {
|
||||||
|
dateTimeFormat = dateTimeFormat.replace('%ms', 'SSS');
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: any;
|
||||||
|
if (source.includes('.')) {
|
||||||
|
value = getValueByPath(item.json, source);
|
||||||
|
} else {
|
||||||
|
value = findValueDeep(item.json, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`Source field "${source}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: string | number;
|
||||||
|
if (mode === 'unixtime') {
|
||||||
|
const ts = Number(value);
|
||||||
|
if (isNaN(ts)) throw new Error(`Value "${value}" is not a valid number`);
|
||||||
|
result = DateTime.fromMillis(ts).toFormat(dateTimeFormat);
|
||||||
|
} else if (mode === 'time') {
|
||||||
|
const dt = DateTime.fromFormat(String(value), dateTimeFormat);
|
||||||
|
if (!dt.isValid) throw new Error(`Invalid date/time format for value "${value}"`);
|
||||||
|
result = dt.toMillis();
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown mode: ${mode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dest.includes('.')) {
|
||||||
|
setValueByPath(item.json, dest, result);
|
||||||
|
} else {
|
||||||
|
item.json[dest] = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Полная перезапись METADATA
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
|
||||||
|
item.json.METADATA = {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.prepareOutputData([item]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Вспомогательные функции ---
|
||||||
|
function getValueByPath(obj: any, path: string): any {
|
||||||
|
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValueByPath(obj: any, path: string, value: any): void {
|
||||||
|
const keys = path.split('.');
|
||||||
|
let current = obj;
|
||||||
|
keys.forEach((key, index) => {
|
||||||
|
if (index === keys.length - 1) {
|
||||||
|
current[key] = value;
|
||||||
|
} else {
|
||||||
|
if (!current[key] || typeof current[key] !== 'object') {
|
||||||
|
current[key] = {};
|
||||||
|
}
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findValueDeep(obj: any, field: string): any {
|
||||||
|
if (obj && typeof obj === 'object') {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, field)) {
|
||||||
|
return obj[field];
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(obj)) {
|
||||||
|
const val = obj[key];
|
||||||
|
if (typeof val === 'object') {
|
||||||
|
const found = findValueDeep(val, field);
|
||||||
|
if (found !== undefined) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
127
custom_nodes_for_n8n-master/nodes/_WSDL/_WSDL.node.ts
Normal file
127
custom_nodes_for_n8n-master/nodes/_WSDL/_WSDL.node.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { IExecuteFunctions } from 'n8n-workflow';
|
||||||
|
import {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeConnectionType,
|
||||||
|
IDataObject,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
export class _WSDL implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'WSDL',
|
||||||
|
name: 'wsdl',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Call a WSDL SOAP function and return the result',
|
||||||
|
defaults: {
|
||||||
|
name: 'WSDL',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'WSDL URI',
|
||||||
|
name: 'uri',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'https://example.com/service?wsdl',
|
||||||
|
description: 'WSDL endpoint URI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Function',
|
||||||
|
name: 'function',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'SOAP function to call (e.g., CarList, StatusToString)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Parameter field in METADATA (optional)',
|
||||||
|
name: 'paramField',
|
||||||
|
type: 'string',
|
||||||
|
default: 'WSDLparam',
|
||||||
|
description: 'Имя поля в METADATA, которое передается как параметр функции',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
if (!items || items.length === 0) return [[]];
|
||||||
|
|
||||||
|
const json = items[0].json as IDataObject;
|
||||||
|
|
||||||
|
const uri = this.getNodeParameter('uri', 0) as string;
|
||||||
|
const func = this.getNodeParameter('function', 0) as string;
|
||||||
|
const paramField = this.getNodeParameter('paramField', 0) as string;
|
||||||
|
|
||||||
|
// Берём параметр из старого METADATA (если был)
|
||||||
|
let paramValue = '';
|
||||||
|
if (json.METADATA && typeof json.METADATA === 'object' && (json.METADATA as IDataObject)[paramField]) {
|
||||||
|
paramValue = String((json.METADATA as IDataObject)[paramField]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SOAP-заготовки
|
||||||
|
const req1 = `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:scales_controllerserver_hostIntf-Iscales_controllerserver_host">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>`;
|
||||||
|
const req2 = `</soapenv:Body></soapenv:Envelope>`;
|
||||||
|
|
||||||
|
// Формируем тело запроса
|
||||||
|
let requestBody = '';
|
||||||
|
if (func === 'CarList') {
|
||||||
|
if (!paramValue) throw new Error('METADATA.' + paramField + ' is required for CarList');
|
||||||
|
requestBody = `${req1}<urn:CarList><Index>${paramValue}</Index></urn:CarList>${req2}`;
|
||||||
|
} else if (func === 'StatusToString') {
|
||||||
|
if (!paramValue) throw new Error('METADATA.' + paramField + ' is required for StatusToString');
|
||||||
|
requestBody = `${req1}<urn:StatusToString><CodeError>${paramValue}</CodeError></urn:StatusToString>${req2}`;
|
||||||
|
} else {
|
||||||
|
requestBody = `${req1}<urn:${func}/>${req2}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем POST
|
||||||
|
const resp = await fetch(uri, {
|
||||||
|
method: 'POST',
|
||||||
|
body: requestBody,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/xml; charset=UTF-8',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const respText = await resp.text();
|
||||||
|
|
||||||
|
// Пробуем распарсить JSON, иначе строка
|
||||||
|
let bodyData: any;
|
||||||
|
try {
|
||||||
|
bodyData = JSON.parse(respText);
|
||||||
|
} catch {
|
||||||
|
bodyData = respText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заполняем DATA
|
||||||
|
json.DATA = {
|
||||||
|
WSDL: {
|
||||||
|
status_code: resp.status,
|
||||||
|
body: bodyData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Полная перезапись METADATA в стиле BaseNodeTransform
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
|
||||||
|
json.METADATA = {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
function: func,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.prepareOutputData([items[0]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { INodeType, INodeTypeDescription, INodeExecutionData, NodeConnectionType, IExecuteFunctions, IDataObject } from 'n8n-workflow';
|
||||||
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
|
|
||||||
|
interface XmlData {
|
||||||
|
body?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _Xml2json implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Xml2json',
|
||||||
|
name: 'xml2json',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Convert XML string in DATA.body to JSON',
|
||||||
|
defaults: { name: 'Xml2json' },
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
properties: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
if (!items || items.length === 0) return [[]];
|
||||||
|
|
||||||
|
const item = items[0];
|
||||||
|
const data = (item.json.DATA as XmlData) || {};
|
||||||
|
|
||||||
|
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
let xmlStr = '';
|
||||||
|
if (data.body && typeof data.body === 'string') {
|
||||||
|
xmlStr = data.body;
|
||||||
|
data.body = undefined;
|
||||||
|
} else if (typeof data === 'string') {
|
||||||
|
xmlStr = data;
|
||||||
|
} else if (data) {
|
||||||
|
xmlStr = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const xmlStart = xmlStr.indexOf('<?xml');
|
||||||
|
if (xmlStart === -1) throw new Error('XML Document is not found');
|
||||||
|
|
||||||
|
const xmlEnd = xmlStr.lastIndexOf('>') + 1;
|
||||||
|
let xmlContent = xmlStr.substring(xmlStart, xmlEnd).replace(/\r/g, '').replace(/\n/g, '');
|
||||||
|
|
||||||
|
const jsonObj = parser.parse(xmlContent);
|
||||||
|
|
||||||
|
// Перезаписываем DATA
|
||||||
|
item.json.DATA = { ...data, ...jsonObj };
|
||||||
|
|
||||||
|
// Полная перезапись METADATA в стиле BaseNodeTransform
|
||||||
|
const workflowName = this.getWorkflow().name;
|
||||||
|
const nodeInfo = this.getNode();
|
||||||
|
const fromNodeFull = nodeInfo.type;
|
||||||
|
const fromNode = fromNodeFull.includes('.') ? fromNodeFull.split('.').pop()! : fromNodeFull;
|
||||||
|
const idField = nodeInfo.name;
|
||||||
|
|
||||||
|
item.json.METADATA = {
|
||||||
|
chain: workflowName,
|
||||||
|
from: fromNode,
|
||||||
|
id: idField,
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Xml2json error: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prepareOutputData([item]);
|
||||||
|
}
|
||||||
|
}
|
||||||
4492
custom_nodes_for_n8n-master/package-lock.json
generated
Normal file
4492
custom_nodes_for_n8n-master/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
custom_nodes_for_n8n-master/package.json
Normal file
29
custom_nodes_for_n8n-master/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "custom-nodes",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"n8n": {
|
||||||
|
"nodes": "dist"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fast-xml-parser": "^5.2.5",
|
||||||
|
"luxon": "^3.7.1",
|
||||||
|
"mqtt": "^5.14.0",
|
||||||
|
"n8n-workflow": "^1.82.0",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
|
"node-fetch": "^2.7.0",
|
||||||
|
"node-opcua": "^2.156.0",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"redis": "^5.6.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/luxon": "^3.7.1",
|
||||||
|
"@types/node": "^20.5.1",
|
||||||
|
"@types/node-fetch": "^2.6.13",
|
||||||
|
"@types/pg": "^8.15.5",
|
||||||
|
"@types/redis": "^4.0.10",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
custom_nodes_for_n8n-master/tsconfig.json
Normal file
16
custom_nodes_for_n8n-master/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
|
||||||
|
"rootDir": "./",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"include": ["nodes/**/*.ts"]
|
||||||
|
}
|
||||||
135
docker-compose.yml
Executable file
135
docker-compose.yml
Executable file
|
|
@ -0,0 +1,135 @@
|
||||||
|
services:
|
||||||
|
n8n:
|
||||||
|
image: n8n/n8n
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: no
|
||||||
|
ports:
|
||||||
|
- "5678:5678"
|
||||||
|
environment:
|
||||||
|
- N8N_FEATURE_FLAG_PROJECTS=true
|
||||||
|
- N8N_PROJECT_MANAGEMENT_STORAGE_TYPE=filesystem
|
||||||
|
- N8N_PROJECT_MANAGEMENT_STORAGE_PATH=/home/node/.n8n/projects
|
||||||
|
- NODE_FUNCTION_ALLOW_BUILTIN=*
|
||||||
|
- NODE_FUNCTION_ALLOW_EXTERNAL=node-opcua,redis
|
||||||
|
- REDIS_HOST=dev.re.promuc.local
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- dbhost=dev.re.promuc.local
|
||||||
|
- dbname=promuc
|
||||||
|
- dbpassword=changeme
|
||||||
|
- dbuser=postgres
|
||||||
|
- dbport=5432
|
||||||
|
|
||||||
|
- mqtthost=dev.re.promuc.local
|
||||||
|
- mqttport=1883
|
||||||
|
- mqttpassword=user
|
||||||
|
- mqttuser=user
|
||||||
|
|
||||||
|
- OPChost=dev.re.promuc.local
|
||||||
|
- OPCport=4840
|
||||||
|
|
||||||
|
- OPCobjects=dev_re_promuc_local
|
||||||
|
- OWEN_host=10.186.6.31
|
||||||
|
- OWEN_port=4840
|
||||||
|
- PromTVinfo=http://10.186.6.21/device/info/all
|
||||||
|
- PGRST_HOST=dev.re.promuc.local
|
||||||
|
- PGRST_PORT=3000
|
||||||
|
- N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom/nodes
|
||||||
|
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
|
||||||
|
- N8N_RUNNERS_ENABLED=true
|
||||||
|
- N8N_DIAGNOSTICS_ENABLED=false
|
||||||
|
- N8N_HIDE_USAGE_SURVEY=true
|
||||||
|
volumes:
|
||||||
|
- ./n8n_data:/home/node/.n8n
|
||||||
|
- ./custom_nodes_for_n8n-master/dist/nodes:/home/node/.n8n/custom/nodes
|
||||||
|
|
||||||
|
extra_hosts:
|
||||||
|
- "dev.re.promuc.local:10.186.1.203"
|
||||||
|
zookeeper:
|
||||||
|
image: confluentinc/cp-zookeeper:7.2.1
|
||||||
|
hostname: zookeeper
|
||||||
|
container_name: zookeeper
|
||||||
|
ports:
|
||||||
|
- "2181:2181"
|
||||||
|
environment:
|
||||||
|
ZOOKEEPER_CLIENT_PORT: 2181
|
||||||
|
ZOOKEEPER_TICK_TIME: 2000
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
image: confluentinc/cp-server:7.2.1
|
||||||
|
hostname: kafka
|
||||||
|
restart: on-failure
|
||||||
|
container_name: kafka
|
||||||
|
depends_on:
|
||||||
|
- zookeeper
|
||||||
|
ports:
|
||||||
|
- "9092:9092"
|
||||||
|
- "9997:9997"
|
||||||
|
environment:
|
||||||
|
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
|
||||||
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
||||||
|
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
|
||||||
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||||
|
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
|
||||||
|
KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1
|
||||||
|
KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1
|
||||||
|
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
||||||
|
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
||||||
|
KAFKA_JMX_PORT: 9997
|
||||||
|
KAFKA_JMX_HOSTNAME: kafka
|
||||||
|
|
||||||
|
kafka-ui:
|
||||||
|
container_name: kafka-ui
|
||||||
|
image: provectuslabs/kafka-ui:latest
|
||||||
|
ports:
|
||||||
|
- 8082:8080
|
||||||
|
environment:
|
||||||
|
DYNAMIC_CONFIG_ENABLED: true
|
||||||
|
KAFKA_CLUSTERS_0_NAME: local
|
||||||
|
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
|
||||||
|
depends_on:
|
||||||
|
- kafka
|
||||||
|
|
||||||
|
kibana:
|
||||||
|
image: kibana:7.16.1
|
||||||
|
container_name: kib
|
||||||
|
ports:
|
||||||
|
- "5601:5601"
|
||||||
|
depends_on:
|
||||||
|
- elasticsearch
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
|
||||||
|
schema-registry:
|
||||||
|
image: confluentinc/cp-schema-registry:7.2.1
|
||||||
|
hostname: schema-registry
|
||||||
|
container_name: schema-registry
|
||||||
|
depends_on:
|
||||||
|
- zookeeper
|
||||||
|
- kafka
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
environment:
|
||||||
|
SCHEMA_REGISTRY_HOST_NAME: schema-registry
|
||||||
|
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'PLAINTEXT://kafka:29092'
|
||||||
|
SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081
|
||||||
|
SCHEMA_REGISTRY_ADVERTISED_LISTENERS: 'PLAINTEXT://schema-registry:8081'
|
||||||
|
|
||||||
|
elasticsearch:
|
||||||
|
image: elasticsearch:7.16.1
|
||||||
|
container_name: elasticsearch
|
||||||
|
environment:
|
||||||
|
discovery.type: single-node
|
||||||
|
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
||||||
|
ports:
|
||||||
|
- "9200:9200"
|
||||||
|
- "9300:9300"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
|
||||||
3
n8n_data/config
Normal file
3
n8n_data/config
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"encryptionKey": "pmk5oJi+piEp1JIALZHC18bYJv7ff4Dm"
|
||||||
|
}
|
||||||
0
n8n_data/crash.journal
Normal file
0
n8n_data/crash.journal
Normal file
43
n8n_data/custom_nodes/ReturnTextNode/ReturnTextNode.ts
Normal file
43
n8n_data/custom_nodes/ReturnTextNode/ReturnTextNode.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { INodeType, INodeTypeDescription, INodeExecutionData, IExecuteFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class ReturnTextNode implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Return Text',
|
||||||
|
name: 'returnTextNode',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Returns user input as text',
|
||||||
|
defaults: {
|
||||||
|
name: 'Return Text',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Text to Return',
|
||||||
|
name: 'message',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'Type your message here...',
|
||||||
|
required: true,
|
||||||
|
description: 'Message to return',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const returnData: INodeExecutionData[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.getInputData().length; i++) {
|
||||||
|
const message = this.getNodeParameter('message', i) as string;
|
||||||
|
|
||||||
|
returnData.push({
|
||||||
|
json: {
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [returnData];
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
n8n_data/database.sqlite
Normal file
BIN
n8n_data/database.sqlite
Normal file
Binary file not shown.
18
n8n_data/n8nEventLog-1.log
Normal file
18
n8n_data/n8nEventLog-1.log
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"38fb003c-e637-4784-a6ca-26354fd6adef","ts":"2025-08-14T09:20:16.277+00:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"16517","workflowId":"qiPDdm8n03EB1TEH","isManual":false,"workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"38fb003c-e637-4784-a6ca-26354fd6adef","ts":"2025-08-14T09:20:16.277+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"bba6c554-26aa-4760-9e75-3a72ff02161a","ts":"2025-08-14T09:20:16.278+00:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16517","nodeType":"CUSTOM.generatorNode","nodeName":"Generator","nodeId":"0a0d7e4e-f0b5-4700-85cd-8c07800c94d7"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"bba6c554-26aa-4760-9e75-3a72ff02161a","ts":"2025-08-14T09:20:16.278+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"0f223c3f-87c9-483a-a76c-d93d888cbd51","ts":"2025-08-14T09:20:17.284+00:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16517","nodeType":"CUSTOM.generatorNode","nodeName":"Generator","nodeId":"0a0d7e4e-f0b5-4700-85cd-8c07800c94d7"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"0f223c3f-87c9-483a-a76c-d93d888cbd51","ts":"2025-08-14T09:20:17.284+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"dc742c96-820a-4321-b2cf-ee4301f06af3","ts":"2025-08-14T09:20:17.285+00:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","executionId":"16517","success":true,"isManual":true,"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"dc742c96-820a-4321-b2cf-ee4301f06af3","ts":"2025-08-14T09:20:17.285+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"3aca5b31-2111-4c9d-93a9-1b96be5ef1cc","ts":"2025-08-14T09:20:44.070+00:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"16518","workflowId":"qiPDdm8n03EB1TEH","isManual":false,"workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"3aca5b31-2111-4c9d-93a9-1b96be5ef1cc","ts":"2025-08-14T09:20:44.070+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"c89133f4-e945-439c-a691-11ab03924227","ts":"2025-08-14T09:20:44.070+00:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16518","nodeType":"CUSTOM.opcWrite","nodeName":"OPCWrite","nodeId":"402da9a8-56fc-490b-9b43-ebd0b611a4d2"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"c89133f4-e945-439c-a691-11ab03924227","ts":"2025-08-14T09:20:44.070+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"38b53f14-3e99-4f53-b72b-e3f6273d6ffa","ts":"2025-08-14T09:20:44.099+00:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16518","nodeType":"CUSTOM.opcWrite","nodeName":"OPCWrite","nodeId":"402da9a8-56fc-490b-9b43-ebd0b611a4d2"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"38b53f14-3e99-4f53-b72b-e3f6273d6ffa","ts":"2025-08-14T09:20:44.099+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"60006334-1803-428a-bd2e-ce181a2366f9","ts":"2025-08-14T09:20:44.099+00:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","executionId":"16518","success":false,"isManual":true,"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","lastNodeExecuted":"OPCWrite","errorMessage":"this.replaceEnvVars is not a function"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"60006334-1803-428a-bd2e-ce181a2366f9","ts":"2025-08-14T09:20:44.100+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageAudit","id":"1b591b32-676c-430a-8bdd-845a004f5530","ts":"2025-08-14T09:20:47.190+00:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","_email":"leoleonkis3@gmail.com","_firstName":"Андрей","_lastName":"Киселёв","globalRole":"global:owner","workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"1b591b32-676c-430a-8bdd-845a004f5530","ts":"2025-08-14T09:20:47.190+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
16
n8n_data/n8nEventLog-2.log
Normal file
16
n8n_data/n8nEventLog-2.log
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"c8e63be8-25fc-4dc5-badd-a2535cda9e50","ts":"2025-08-14T09:15:00.048+00:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"16515","workflowId":"qiPDdm8n03EB1TEH","isManual":false,"workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"c8e63be8-25fc-4dc5-badd-a2535cda9e50","ts":"2025-08-14T09:15:00.048+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"33ec76e7-8733-4bb2-85b1-1c6a46333c91","ts":"2025-08-14T09:15:00.049+00:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16515","nodeType":"CUSTOM.generatorNode","nodeName":"Generator","nodeId":"19ea6183-2637-4cde-9553-bc7a51337b95"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"33ec76e7-8733-4bb2-85b1-1c6a46333c91","ts":"2025-08-14T09:15:00.049+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"c4cb4c24-9f8e-47ac-a95a-4552d0853689","ts":"2025-08-14T09:15:01.055+00:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16515","nodeType":"CUSTOM.generatorNode","nodeName":"Generator","nodeId":"19ea6183-2637-4cde-9553-bc7a51337b95"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"c4cb4c24-9f8e-47ac-a95a-4552d0853689","ts":"2025-08-14T09:15:01.055+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"05fde6c1-705e-4253-a680-75a2e7bbcebb","ts":"2025-08-14T09:15:01.060+00:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","executionId":"16515","success":true,"isManual":true,"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"05fde6c1-705e-4253-a680-75a2e7bbcebb","ts":"2025-08-14T09:15:01.061+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"a5d16b8d-7c25-4bde-a06f-3014bc549338","ts":"2025-08-14T09:15:49.583+00:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"16516","workflowId":"qiPDdm8n03EB1TEH","isManual":false,"workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"a5d16b8d-7c25-4bde-a06f-3014bc549338","ts":"2025-08-14T09:15:49.583+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"be7de743-7e33-4e76-882c-1ce21776e04b","ts":"2025-08-14T09:15:49.584+00:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16516","nodeType":"CUSTOM.opcWrite","nodeName":"OPCWrite","nodeId":"33f19896-7f56-44fc-a8fe-fac6a145d0e6"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"be7de743-7e33-4e76-882c-1ce21776e04b","ts":"2025-08-14T09:15:49.584+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"6324a10e-bbbd-413a-b766-00b31130afff","ts":"2025-08-14T09:15:49.611+00:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16516","nodeType":"CUSTOM.opcWrite","nodeName":"OPCWrite","nodeId":"33f19896-7f56-44fc-a8fe-fac6a145d0e6"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"6324a10e-bbbd-413a-b766-00b31130afff","ts":"2025-08-14T09:15:49.611+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"8bd8e441-b933-41a5-9bee-07600dba068d","ts":"2025-08-14T09:15:49.612+00:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","executionId":"16516","success":false,"isManual":true,"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","lastNodeExecuted":"OPCWrite","errorMessage":"self.replaceEnvVars is not a function"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"8bd8e441-b933-41a5-9bee-07600dba068d","ts":"2025-08-14T09:15:49.612+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
26
n8n_data/n8nEventLog-3.log
Normal file
26
n8n_data/n8nEventLog-3.log
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{"__type":"$$EventMessageAudit","id":"e34c4bac-a387-4489-bd8c-1b6316cb75e7","ts":"2025-08-14T08:51:24.530+00:00","eventName":"n8n.audit.workflow.updated","message":"n8n.audit.workflow.updated","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","_email":"leoleonkis3@gmail.com","_firstName":"Андрей","_lastName":"Киселёв","globalRole":"global:owner","workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"e34c4bac-a387-4489-bd8c-1b6316cb75e7","ts":"2025-08-14T08:51:24.531+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"62132895-92f2-426a-92f5-319fa7bdc430","ts":"2025-08-14T08:55:22.253+00:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"16512","workflowId":"qiPDdm8n03EB1TEH","isManual":false,"workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"62132895-92f2-426a-92f5-319fa7bdc430","ts":"2025-08-14T08:55:22.253+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"d2073fa0-5041-4a81-9a11-1193f7f5c93c","ts":"2025-08-14T08:55:22.254+00:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16512","nodeType":"CUSTOM.generatorNode","nodeName":"Generator","nodeId":"deee8837-17a9-4c7a-8468-fdc2ab61c85d"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"d2073fa0-5041-4a81-9a11-1193f7f5c93c","ts":"2025-08-14T08:55:22.254+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"66d38d8a-e3b8-406b-843a-be36a7ce8c88","ts":"2025-08-14T08:55:23.260+00:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16512","nodeType":"CUSTOM.generatorNode","nodeName":"Generator","nodeId":"deee8837-17a9-4c7a-8468-fdc2ab61c85d"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"66d38d8a-e3b8-406b-843a-be36a7ce8c88","ts":"2025-08-14T08:55:23.261+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"40e14060-1eb3-49a9-a552-f01e4306a34f","ts":"2025-08-14T08:55:23.266+00:00","eventName":"n8n.workflow.success","message":"n8n.workflow.success","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","executionId":"16512","success":true,"isManual":true,"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"40e14060-1eb3-49a9-a552-f01e4306a34f","ts":"2025-08-14T08:55:23.266+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"de480afa-7d0a-4f59-858e-d0cebe02b90d","ts":"2025-08-14T09:01:56.440+00:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"16513","workflowId":"qiPDdm8n03EB1TEH","isManual":false,"workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"de480afa-7d0a-4f59-858e-d0cebe02b90d","ts":"2025-08-14T09:01:56.440+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"90f25449-0048-4cc3-aa02-7a6929644528","ts":"2025-08-14T09:01:56.441+00:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16513","nodeType":"CUSTOM.opcWrite","nodeName":"OPCWrite","nodeId":"d6d1c615-490f-426c-9703-f350efbde40a"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"90f25449-0048-4cc3-aa02-7a6929644528","ts":"2025-08-14T09:01:56.441+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"4507ba1a-0bdd-4a7b-8527-53ecbc312d0f","ts":"2025-08-14T09:01:56.472+00:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16513","nodeType":"CUSTOM.opcWrite","nodeName":"OPCWrite","nodeId":"d6d1c615-490f-426c-9703-f350efbde40a"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"4507ba1a-0bdd-4a7b-8527-53ecbc312d0f","ts":"2025-08-14T09:01:56.472+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"326c245c-db65-4ab4-a4ea-3591fcb4eaf2","ts":"2025-08-14T09:01:56.473+00:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","executionId":"16513","success":false,"isManual":true,"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","lastNodeExecuted":"OPCWrite","errorMessage":"this.replaceEnvVars is not a function"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"326c245c-db65-4ab4-a4ea-3591fcb4eaf2","ts":"2025-08-14T09:01:56.473+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"f7e79bd7-0c92-437e-b3c6-acc50ae1c1b5","ts":"2025-08-14T09:02:34.541+00:00","eventName":"n8n.workflow.started","message":"n8n.workflow.started","payload":{"executionId":"16514","workflowId":"qiPDdm8n03EB1TEH","isManual":false,"workflowName":"My workflow 3"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"f7e79bd7-0c92-437e-b3c6-acc50ae1c1b5","ts":"2025-08-14T09:02:34.541+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"8773f968-15a1-4ea3-a738-e3e41f2db407","ts":"2025-08-14T09:02:34.542+00:00","eventName":"n8n.node.started","message":"n8n.node.started","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16514","nodeType":"CUSTOM.opcWrite","nodeName":"OPCWrite","nodeId":"d6d1c615-490f-426c-9703-f350efbde40a"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"8773f968-15a1-4ea3-a738-e3e41f2db407","ts":"2025-08-14T09:02:34.542+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageNode","id":"dcf070b2-64e5-4e89-8a87-0afd633449d2","ts":"2025-08-14T09:02:34.544+00:00","eventName":"n8n.node.finished","message":"n8n.node.finished","payload":{"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","executionId":"16514","nodeType":"CUSTOM.opcWrite","nodeName":"OPCWrite","nodeId":"d6d1c615-490f-426c-9703-f350efbde40a"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"dcf070b2-64e5-4e89-8a87-0afd633449d2","ts":"2025-08-14T09:02:34.545+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
|
{"__type":"$$EventMessageWorkflow","id":"d653b888-88f6-4463-8efe-7a32e16176ef","ts":"2025-08-14T09:02:34.545+00:00","eventName":"n8n.workflow.failed","message":"n8n.workflow.failed","payload":{"userId":"a63ef5f0-aafa-4080-ad3f-07f0ef522852","executionId":"16514","success":false,"isManual":true,"workflowId":"qiPDdm8n03EB1TEH","workflowName":"My workflow 3","lastNodeExecuted":"OPCWrite","errorMessage":"this.replaceEnvVars is not a function"}}
|
||||||
|
{"__type":"$$EventMessageConfirm","confirm":"d653b888-88f6-4463-8efe-7a32e16176ef","ts":"2025-08-14T09:02:34.545+00:00","source":{"id":"0","name":"eventBus"}}
|
||||||
0
n8n_data/n8nEventLog.log
Normal file
0
n8n_data/n8nEventLog.log
Normal file
5
n8n_data/nodes/package.json
Executable file
5
n8n_data/nodes/package.json
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "installed-nodes",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user