This commit is contained in:
kiselev 2025-08-14 14:33:09 +04:00
commit bbfc60c096
54 changed files with 8380 additions and 0 deletions

18
Dockerfile Executable file
View 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
View 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>

View 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

View 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',
},
},
],
};

View 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',
},
},
],
};

View File

@ -0,0 +1,8 @@
node_modules
.DS_Store
.tmp
tmp
dist
npm-debug.log*
yarn.lock
.vscode/launch.json

View File

@ -0,0 +1,2 @@
.DS_Store
*.tsbuildinfo

View 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,
};

View File

@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
]
}

View 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

View 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.

View File

@ -0,0 +1,48 @@
![Banner image](https://user-images.githubusercontent.com/10284570/173569848-c624317f-42b1-45a6-ab09-f0ea3c247648.png)
# 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)

View 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._

View 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));
}

View File

@ -0,0 +1,2 @@
export * from './nodes/MyCustomNode/MyCustomNode.node';
export * from './nodes/Generator/Generator.node';

View 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);

View File

@ -0,0 +1,2 @@
export * from './nodes/MyCustomNode/MyCustomNode.node';
export * from './nodes/Generator/Generator.node';

View File

@ -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(),
},
},
})),
];
}
}

View File

@ -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);
},
};
}
}

View 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}`);
}
}
}

View File

@ -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;
}
}

View 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);
}
}

View File

@ -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);
},
};
}
}

View 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, // мы сами ответили клиенту
};
}
}

View 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();
},
};
}
}

View 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]);
}
}

View File

@ -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];
}
}

View 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 };
}
}

View 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);
}
}

View 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);
}
}

View 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]);
}
}

View 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}`);
}
}
}

View File

@ -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(),
},
},
},
]];
}
}

View File

@ -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: 'Часы (023)',
name: 'hour',
type: 'number',
default: 9,
typeOptions: { minValue: 0, maxValue: 23 },
description: 'Время запуска — часы (по серверу)',
},
{
displayName: 'Минуты (059)',
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();
},
};
}
}

View File

@ -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]];
}
}

View File

@ -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]];
}
}

View File

@ -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]];
}
}

View File

@ -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]];
}
}

View File

@ -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;
}

View 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]]);
}
}

View File

@ -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]);
}
}

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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
View 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
View File

@ -0,0 +1,3 @@
{
"encryptionKey": "pmk5oJi+piEp1JIALZHC18bYJv7ff4Dm"
}

0
n8n_data/crash.journal Normal file
View File

View 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

Binary file not shown.

View 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"}}

View 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"}}

View 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
View File

5
n8n_data/nodes/package.json Executable file
View File

@ -0,0 +1,5 @@
{
"name": "installed-nodes",
"private": true,
"dependencies": {}
}