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