├── .eslintrc.json ├── .eslintrc.prepublish.js ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── DEBUG.md ├── LICENSE ├── README.md ├── api-docs-v2.35.0.json ├── api-docs-v2.35.3.json ├── api-docs-v2.36.0.json ├── api-docs-v2.36.3.json ├── api-docs-v2.37.0.beta.json ├── credentials └── HuduApi.credentials.ts ├── gulpfile.js ├── index.ts ├── package.json ├── src └── nodes │ └── Hudu │ ├── Hudu.node.json │ ├── Hudu.node.ts │ ├── descriptions │ ├── HuduAssetCustomFieldDescription.ts │ ├── HuduAssetLinkFieldDescription.ts │ ├── HuduAssetStandardFieldDescription.ts │ ├── activity_logs.description.ts │ ├── api_info.description.ts │ ├── articles.description.ts │ ├── asset_layout_fields.description.ts │ ├── asset_layouts.description.ts │ ├── asset_passwords.description.ts │ ├── assets.description.ts │ ├── cards.description.ts │ ├── companies.description.ts │ ├── expirations.description.ts │ ├── folders.description.ts │ ├── index.ts │ ├── ip_addresses.description.ts │ ├── list_options.description.ts │ ├── lists.description.ts │ ├── magic_dash.description.ts │ ├── matchers.description.ts │ ├── networks.description.ts │ ├── passwordFolders.descriptions.ts │ ├── procedure_tasks.description.ts │ ├── procedures.description.ts │ ├── public_photos.description.ts │ ├── rack_storage_items.description.ts │ ├── rack_storages.description.ts │ ├── relations.description.ts │ ├── resources.ts │ ├── uploads.description.ts │ ├── users.description.ts │ ├── vlan_zones.description.ts │ ├── vlans.description.ts │ └── websites.description.ts │ ├── docs │ └── FILTERS.md │ ├── hudu.svg │ ├── optionLoaders │ ├── asset_layouts │ │ ├── getAssetLayoutFieldValues.ts │ │ ├── getAssetLayoutFields.ts │ │ ├── getAssetLayouts.ts │ │ ├── getAssetLinkFields.ts │ │ ├── getCustomFieldsLayoutFields.ts │ │ └── index.ts │ ├── assets │ │ ├── getAssetCustomFields.ts │ │ ├── getAssets.ts │ │ ├── getAssetsForCompany.ts │ │ ├── getLinkableAssets.ts │ │ └── index.ts │ ├── companies │ │ ├── getCompanies.ts │ │ └── index.ts │ ├── index.ts │ ├── lists │ │ ├── getLists.ts │ │ └── index.ts │ └── users │ │ ├── getUsers.ts │ │ └── index.ts │ ├── resources │ ├── activity_logs │ │ ├── activity_logs.handler.ts │ │ └── activity_logs.types.ts │ ├── api_info │ │ ├── api_info.handler.ts │ │ └── api_info.types.ts │ ├── articles │ │ ├── articles.handler.ts │ │ └── articles.types.ts │ ├── assetCustomField │ │ ├── assetCustomField.handler.ts │ │ └── assetCustomField.types.ts │ ├── assetLinkField │ │ ├── assetLinkField.handler.ts │ │ └── assetLinkField.types.ts │ ├── assetStandardField │ │ ├── assetStandardField.handler.ts │ │ └── assetStandardField.types.ts │ ├── asset_layout_fields │ │ ├── asset_layout_fields.handler.ts │ │ └── asset_layout_fields.types.ts │ ├── asset_layouts │ │ ├── asset_layouts.handler.ts │ │ └── asset_layouts.types.ts │ ├── asset_passwords │ │ ├── asset_passwords.handler.ts │ │ └── asset_passwords.types.ts │ ├── assets │ │ ├── assets.handler.ts │ │ └── assets.types.ts │ ├── cards │ │ ├── cards.handler.ts │ │ └── cards.types.ts │ ├── companies │ │ ├── companies.handler.ts │ │ └── companies.types.ts │ ├── expirations │ │ ├── expirations.handler.ts │ │ └── expirations.types.ts │ ├── folders │ │ ├── folders.handler.ts │ │ └── folders.types.ts │ ├── index.ts │ ├── ip_addresses │ │ ├── ip_addresses.handler.ts │ │ └── ip_addresses.types.ts │ ├── list_options │ │ ├── list_options.handler.ts │ │ └── list_options.types.ts │ ├── lists │ │ ├── lists.handler.ts │ │ └── lists.types.ts │ ├── magic_dash │ │ ├── magic_dash.handler.ts │ │ └── magic_dash.types.ts │ ├── matchers │ │ ├── matchers.handler.ts │ │ └── matchers.types.ts │ ├── networks │ │ ├── networks.handler.ts │ │ └── networks.types.ts │ ├── password_folders │ │ ├── password_folders.handler.ts │ │ └── password_folders.types.ts │ ├── procedure_tasks │ │ ├── procedure_tasks.handler.ts │ │ └── procedure_tasks.types.ts │ ├── procedures │ │ ├── procedures.handler.ts │ │ └── procedures.types.ts │ ├── public_photos │ │ ├── public_photos.handler.ts │ │ └── public_photos.types.ts │ ├── rack_storage_items │ │ ├── rack_storage_items.handler.ts │ │ └── rack_storage_items.types.ts │ ├── rack_storages │ │ ├── rack_storages.handler.ts │ │ └── rack_storages.types.ts │ ├── relations │ │ ├── relations.handler.ts │ │ └── relations.types.ts │ ├── uploads │ │ ├── uploads.handler.ts │ │ └── uploads.types.ts │ ├── users │ │ ├── users.handler.ts │ │ └── users.types.ts │ ├── vlan_zones │ │ ├── vlan_zones.handler.ts │ │ └── vlan_zones.types.ts │ ├── vlans │ │ ├── vlans.handler.ts │ │ └── vlans.types.ts │ └── websites │ │ ├── websites.handler.ts │ │ └── websites.types.ts │ └── utils │ ├── constants.ts │ ├── dateUtils.ts │ ├── debugConfig.ts │ ├── filterUtils.ts │ ├── formatters.ts │ ├── index.ts │ ├── operations │ ├── archive.ts │ ├── cards.ts │ ├── companies.ts │ ├── create.ts │ ├── delete.ts │ ├── get.ts │ ├── getAll.ts │ ├── getCompanyIdForAsset.ts │ ├── index.ts │ ├── magic_dash.ts │ ├── matchers.ts │ ├── procedures.ts │ ├── systemInfo.ts │ └── update.ts │ ├── requestUtils.ts │ ├── types.ts │ └── validation.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json", 5 | "extraFileExtensions": [".json"] 6 | }, 7 | "extends": [ 8 | "plugin:n8n-nodes-base/nodes" 9 | ], 10 | "plugins": ["eslint-plugin-n8n-nodes-base"], 11 | "rules": { 12 | "n8n-nodes-base/node-param-default-missing": "off", 13 | "n8n-nodes-base/node-param-description-wrong-for-return-all": "off", 14 | "n8n-nodes-base/node-class-description-inputs-wrong-regular-node": "off", 15 | "n8n-nodes-base/node-param-display-name-wrong-for-update-fields": "off" 16 | }, 17 | "overrides": [ 18 | { 19 | "files": ["*.json"], 20 | "parser": "jsonc-eslint-parser" 21 | }, 22 | { 23 | "files": ["*.js"], 24 | "parser": "espree" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.eslintrc.prepublish.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['.eslintrc.js'], 3 | rules: { 4 | 'n8n-nodes-base/node-param-resource-with-plural-option': 'off' 5 | } 6 | }; -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | buy_me_a_coffee: msoukhomlinov 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | package-lock.json 7 | .package-lock.json 8 | pnpm-lock.yaml 9 | yarn.lock 10 | .npm 11 | .npmrc 12 | 13 | # Build outputs 14 | dist/ 15 | build/ 16 | *.tsbuildinfo 17 | *.map 18 | *.d.ts 19 | 20 | # IDE and editor 21 | .idea/ 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | .sublime-* 29 | .vscode/* 30 | *.swp 31 | *.swo 32 | *.suo 33 | *.ntvs* 34 | *.njsproj 35 | *.sln 36 | 37 | # Environment variables 38 | .env 39 | .env.* 40 | !.env.example 41 | .env.test 42 | 43 | # OS specific 44 | .DS_Store 45 | Thumbs.db 46 | 47 | # Cache 48 | .cache/ 49 | .eslintcache 50 | 51 | # Temporary files 52 | temp/ 53 | tmp/ 54 | .tmp/ 55 | .temp/ 56 | *.tmp 57 | 58 | # Cursor specific (since you're using Cursor IDE) 59 | .cursorcache 60 | .cursorconfig 61 | .cursorignore 62 | .cursor/ 63 | .cursor-rules/ 64 | .cursor-search-cache 65 | .cursor-settings.json 66 | .cursor-keybindings.json 67 | .cursor-snippets/ 68 | .cursor-prompts/ 69 | .cursorrules 70 | .cursorignore 71 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Max Soukhomlinov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /credentials/HuduApi.credentials.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IAuthenticateGeneric, 3 | ICredentialTestRequest, 4 | ICredentialType, 5 | INodeProperties, 6 | } from 'n8n-workflow'; 7 | 8 | export class HuduApi implements ICredentialType { 9 | name = 'huduApi'; 10 | displayName = 'Hudu API'; 11 | properties: INodeProperties[] = [ 12 | { 13 | displayName: 'Base URL', 14 | name: 'baseUrl', 15 | type: 'string', 16 | default: '', 17 | placeholder: 'https://your-hudu-instance', 18 | required: true, 19 | }, 20 | { 21 | displayName: 'API Key', 22 | name: 'apiKey', 23 | type: 'string', 24 | typeOptions: { 25 | password: true, 26 | }, 27 | default: '', 28 | required: true, 29 | }, 30 | ]; 31 | 32 | authenticate: IAuthenticateGeneric = { 33 | type: 'generic', 34 | properties: { 35 | headers: { 36 | 'x-api-key': '={{$credentials.apiKey}}', 37 | }, 38 | }, 39 | }; 40 | 41 | test: ICredentialTestRequest = { 42 | request: { 43 | baseURL: '={{$credentials.baseUrl}}', 44 | url: '/api/v1/companies', 45 | method: 'GET', 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const clean = require('gulp-clean'); 3 | 4 | // Clean dist folder 5 | function cleanDist() { 6 | return gulp.src('dist', { 7 | read: false, 8 | allowEmpty: true 9 | }) 10 | .pipe(clean()); 11 | } 12 | 13 | // Copy credentials to dist 14 | function copyCredentials() { 15 | return gulp.src('credentials/**/*') 16 | .pipe(gulp.dest('dist/credentials')); 17 | } 18 | 19 | // Copy node files to dist (including icons and supporting folders) 20 | function copyNodeFiles() { 21 | return gulp.src('src/nodes/Hudu/**/*', { base: 'src' }) 22 | .pipe(gulp.dest('dist')); 23 | } 24 | 25 | // Move compiled files to correct locations and cleanup 26 | function moveCompiledFiles() { 27 | return gulp.src('dist/src/nodes/Hudu/**/*.{js,js.map,json}', { base: 'dist/src' }) 28 | .pipe(gulp.dest('dist')) 29 | .on('end', () => { 30 | gulp.src('dist/src', { read: false, allowEmpty: true }) 31 | .pipe(clean()); 32 | }); 33 | } 34 | 35 | // Organize the build tasks 36 | exports['copy:files'] = gulp.series(copyCredentials, copyNodeFiles); 37 | exports.cleanDist = cleanDist; 38 | exports.move = moveCompiledFiles; 39 | exports.default = gulp.series(cleanDist, copyCredentials, copyNodeFiles, moveCompiledFiles); 40 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import type { INodeType } from 'n8n-workflow'; 2 | import { Hudu } from './src/nodes/Hudu/Hudu.node'; 3 | 4 | // Export the instantiated class 5 | const huduNode = new Hudu(); 6 | export { huduNode as Hudu }; 7 | 8 | export const nodes: INodeType[] = [huduNode]; 9 | 10 | export const credentials = [ 11 | { 12 | name: 'huduApi', 13 | displayName: 'Hudu API', 14 | }, 15 | ]; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "n8n-nodes-hudu", 3 | "version": "1.3.5", 4 | "description": "This n8n custom node facilitates integration with Hudu's API.", 5 | "keywords": [ 6 | "n8n-community-node-package", 7 | "n8n-node-hudu", 8 | "hudu", 9 | "hudu-api", 10 | "knowledgebase-management", 11 | "n8n-custom-node", 12 | "workflow-automation", 13 | "data-operations" 14 | ], 15 | "license": "MIT", 16 | "homepage": "https://github.com/msoukhomlinov/n8n-nodes-hudu", 17 | "author": { 18 | "name": "Max Soukhomlinov", 19 | "email": "maxs@intellectit.com.au" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/msoukhomlinov/n8n-nodes-hudu.git" 24 | }, 25 | "main": "dist/index.js", 26 | "types": "dist/index.d.ts", 27 | "scripts": { 28 | "build": "gulp cleanDist && tsc && gulp copy:files && gulp move", 29 | "dev": "tsc --watch", 30 | "format": "prettier src nodes credentials --write", 31 | "lint": "eslint \"src/**/*.ts\" *.ts package.json", 32 | "lintfix": "eslint \"src/**/*.ts\" *.ts package.json --fix", 33 | "prepublishOnly": "npm run build", 34 | "clean": "gulp cleanDist" 35 | }, 36 | "files": [ 37 | "dist" 38 | ], 39 | "n8n": { 40 | "n8nNodesApiVersion": 1, 41 | "credentials": [ 42 | "dist/credentials/HuduApi.credentials.js" 43 | ], 44 | "nodes": [ 45 | "dist/nodes/Hudu/Hudu.node.js" 46 | ] 47 | }, 48 | "engines": { 49 | "n8n": ">=1.69.2" 50 | }, 51 | "devDependencies": { 52 | "@types/cheerio": "^0.22.35", 53 | "@types/diff": "^7.0.0", 54 | "@types/express": "^4.17.6", 55 | "@types/got": "^9.6.12", 56 | "@types/lodash": "^4.17.14", 57 | "@types/luxon": "^3.4.2", 58 | "@types/node": "^20.17.50", 59 | "@types/node-fetch": "^2.6.11", 60 | "@types/request-promise-native": "~1.0.15", 61 | "@types/turndown": "^5.0.5", 62 | "@typescript-eslint/eslint-plugin": "^5.57.1", 63 | "@typescript-eslint/parser": "~5.45", 64 | "eslint": "^8.37.0", 65 | "eslint-config-prettier": "^8.8.0", 66 | "eslint-plugin-n8n-nodes-base": "^1.16.3", 67 | "eslint-plugin-prettier": "^4.2.1", 68 | "got": "^14.4.5", 69 | "gulp": "^4.0.2", 70 | "gulp-clean": "^0.4.0", 71 | "jsonc-eslint-parser": "^2.4.0", 72 | "n8n-core": "^1.69.2", 73 | "n8n-workflow": "^1.69.2", 74 | "node-fetch": "^2.7.0", 75 | "prettier": "^2.7.1", 76 | "typescript": "^4.9.5" 77 | }, 78 | "dependencies": { 79 | "cheerio": "^1.0.0", 80 | "diff": "^5.1.0", 81 | "html-entities": "^2.5.2", 82 | "html-to-markdown": "^1.0.0", 83 | "lodash": "^4.17.21", 84 | "luxon": "^3.4.4", 85 | "parse5": "^7.1.2" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/nodes/Hudu/Hudu.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-base.hudu", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Documentation"], 6 | "resources": { 7 | "credentialDocumentation": [ 8 | { 9 | "url": "https://support.hudu.com/hc/en-us/articles/11422780787735-REST-API" 10 | } 11 | ], 12 | "primaryDocumentation": [ 13 | { 14 | "url": "https://github.com/msoukhomlinov/n8n-nodes-hudu" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/HuduAssetCustomFieldDescription.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const assetCustomFieldProperties: INodeProperties[] = [ 4 | { 5 | displayName: 'Operation', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | displayOptions: { 10 | show: { 11 | resource: ['assetCustomField'], 12 | }, 13 | }, 14 | options: [ 15 | { 16 | name: 'Get', 17 | value: 'get', 18 | description: 'Retrieve the value of a specific custom field on a Hudu asset', 19 | action: 'Get an asset custom field value', 20 | }, 21 | { 22 | name: 'Update', 23 | value: 'update', 24 | description: 'Modify the value of a specific custom field on a Hudu asset', 25 | action: 'Update an asset custom field value', 26 | }, 27 | ], 28 | default: 'get', 29 | }, 30 | { 31 | displayName: 'Asset ID', 32 | name: 'assetId', 33 | type: 'string', 34 | required: true, 35 | displayOptions: { 36 | show: { 37 | resource: ['assetCustomField'], 38 | }, 39 | }, 40 | default: '', 41 | description: 'The ID of the asset', 42 | }, 43 | { 44 | displayName: 'Custom Field Name or ID', 45 | name: 'fieldIdentifier', 46 | type: 'options', 47 | required: true, 48 | displayOptions: { 49 | show: { 50 | resource: ['assetCustomField'], 51 | }, 52 | }, 53 | typeOptions: { 54 | loadOptionsMethod: 'getAssetCustomFields', 55 | loadOptionsDependsOn: ['assetId'], 56 | }, 57 | default: '', 58 | description: 'Choose from the list, or specify an ID using an expression', 59 | }, 60 | { 61 | displayName: 'Value', 62 | name: 'value', 63 | type: 'string', 64 | default: '', 65 | description: 'The value to set for this field', 66 | displayOptions: { 67 | show: { 68 | resource: ['assetCustomField'], 69 | operation: ['update'], 70 | }, 71 | }, 72 | }, 73 | ]; -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/HuduAssetLinkFieldDescription.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const assetLinkFieldProperties: INodeProperties[] = [ 4 | { 5 | displayName: 'Operation', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | displayOptions: { 10 | show: { 11 | resource: ['assetLinkField'], 12 | }, 13 | }, 14 | options: [ 15 | { 16 | name: 'Get', 17 | value: 'get', 18 | description: 'Retrieve the linked asset(s) for a specific asset link field on a Hudu asset', 19 | action: 'Get an asset link field value', 20 | }, 21 | { 22 | name: 'Update', 23 | value: 'update', 24 | description: 'Modify the linked asset(s) for a specific asset link field on a Hudu asset', 25 | action: 'Update an asset link field value', 26 | }, 27 | ], 28 | default: 'get', 29 | }, 30 | { 31 | displayName: 'Asset ID', 32 | name: 'assetId', 33 | type: 'number', 34 | required: true, 35 | default: undefined, 36 | placeholder: 'Enter the Asset ID', 37 | description: 'The numeric ID of the asset. You can use an expression to specify this dynamically.', 38 | displayOptions: { 39 | show: { 40 | resource: ['assetLinkField'], 41 | }, 42 | }, 43 | }, 44 | { 45 | displayName: 'Link Field Identifier Name or ID', 46 | name: 'fieldIdentifier', 47 | type: 'options', 48 | required: true, 49 | displayOptions: { 50 | show: { 51 | resource: ['assetLinkField'], 52 | }, 53 | }, 54 | typeOptions: { 55 | loadOptionsMethod: 'getAssetLinkFields', 56 | loadOptionsDependsOn: ['assetId'], 57 | }, 58 | default: '', 59 | description: 'Choose from the list, or specify an ID using an expression', 60 | }, 61 | { 62 | displayName: 'Linked Asset ID(s)', 63 | name: 'value', 64 | type: 'string', 65 | default: '', 66 | description: 'A comma-separated string of asset IDs to link (e.g., \'123,456\'). You can use an expression to dynamically set linked assets.', 67 | displayOptions: { 68 | show: { 69 | resource: ['assetLinkField'], 70 | operation: ['update'], 71 | }, 72 | }, 73 | placeholder: 'e.g., 101,202,303', 74 | }, 75 | ]; -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/HuduAssetStandardFieldDescription.ts: -------------------------------------------------------------------------------- 1 | import { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const assetStandardFieldDescription: INodeProperties[] = [ 4 | { 5 | displayName: 'Operation', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | displayOptions: { 10 | show: { 11 | resource: ['assetStandardField'], 12 | }, 13 | }, 14 | options: [ 15 | { 16 | name: 'Get', 17 | value: 'get', 18 | description: 'Retrieve the value of a specific standard field from a Hudu asset', 19 | action: 'Get an asset standard field value', 20 | }, 21 | { 22 | name: 'Update', 23 | value: 'update', 24 | description: 'Modify the value of a specific standard field for a Hudu asset', 25 | action: 'Update an asset standard field value', 26 | }, 27 | ], 28 | default: 'get', 29 | }, 30 | { 31 | displayName: 'Asset Name or ID', 32 | name: 'assetId', 33 | type: 'string', 34 | required: true, 35 | displayOptions: { 36 | show: { 37 | resource: ['assetStandardField'], 38 | }, 39 | }, 40 | default: '', 41 | description: 'Enter the Asset ID. This is a plain input field.', 42 | }, 43 | { 44 | displayName: 'Field Identifier Name or ID', 45 | name: 'fieldIdentifier', 46 | type: 'options', 47 | required: true, 48 | displayOptions: { 49 | show: { 50 | resource: ['assetStandardField'], 51 | }, 52 | }, 53 | typeOptions: { 54 | loadOptionsMethod: 'getStandardAssetFields', 55 | }, 56 | default: '', 57 | description: 'Choose from the list, or specify an ID using an expression', 58 | }, 59 | { 60 | displayName: 'Value', 61 | name: 'value', 62 | type: 'string', 63 | default: '', 64 | description: 'The new value to assign to the selected standard field. This is only applicable for the Update operation. Ensure the value is compatible with the field\'s expected data type (e.g., true/false for Archived, a number for numeric fields).', 65 | displayOptions: { 66 | show: { 67 | resource: ['assetStandardField'], 68 | operation: ['update'], 69 | }, 70 | }, 71 | }, 72 | ]; -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/activity_logs.description.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { HUDU_API_CONSTANTS, ACTIVITY_LOG_ACTIONS, RESOURCE_TYPES } from '../utils/constants'; 3 | import { formatTitleCase } from '../utils/formatters'; 4 | 5 | export const activityLogsOperations: INodeProperties[] = [ 6 | { 7 | displayName: 'Operation', 8 | name: 'operation', 9 | type: 'options', 10 | noDataExpression: true, 11 | displayOptions: { 12 | show: { 13 | resource: ['activity_logs'], 14 | }, 15 | }, 16 | options: [ 17 | { 18 | name: 'Get Many', 19 | value: 'getAll', 20 | description: 'Retrieve a list of activity logs', 21 | action: 'Get many activity logs', 22 | }, 23 | { 24 | name: 'Delete', 25 | value: 'delete', 26 | description: 'Delete activity logs', 27 | action: 'Delete activity logs', 28 | }, 29 | ], 30 | default: 'getAll', 31 | }, 32 | ]; 33 | 34 | export const activityLogsFields: INodeProperties[] = [ 35 | // Fields for getAll operation 36 | { 37 | displayName: 'Return All ⚠️', 38 | name: 'returnAll', 39 | type: 'boolean', 40 | default: false, 41 | description: 42 | 'Whether to return all results or only up to a given limit. Use with caution - may return very high number of records.', 43 | displayOptions: { 44 | show: { 45 | resource: ['activity_logs'], 46 | operation: ['getAll'], 47 | }, 48 | }, 49 | }, 50 | { 51 | displayName: 'Limit', 52 | name: 'limit', 53 | type: 'number', 54 | default: HUDU_API_CONSTANTS.PAGE_SIZE, 55 | description: 'Max number of results to return', 56 | typeOptions: { 57 | minValue: 1, 58 | }, 59 | displayOptions: { 60 | show: { 61 | resource: ['activity_logs'], 62 | operation: ['getAll'], 63 | returnAll: [false], 64 | }, 65 | }, 66 | }, 67 | { 68 | displayName: 'Filters', 69 | name: 'additionalFields', 70 | type: 'collection', 71 | placeholder: 'Add Filter', 72 | default: {}, 73 | displayOptions: { 74 | show: { 75 | resource: ['activity_logs'], 76 | operation: ['getAll'], 77 | }, 78 | }, 79 | description: 'All filters are combined using AND logic', 80 | options: [ 81 | { 82 | displayName: 'Action Message', 83 | name: 'action_message', 84 | type: 'options', 85 | options: ACTIVITY_LOG_ACTIONS.map((action) => ({ 86 | name: formatTitleCase(action), 87 | value: action, 88 | })), 89 | default: '', 90 | description: 'Filter by exact action message match', 91 | }, 92 | { 93 | displayName: 'Resource ID', 94 | name: 'resource_id', 95 | type: 'number', 96 | default: 0, 97 | description: 'Filter by resource ID (must be used together with Resource Type)', 98 | }, 99 | { 100 | displayName: 'Resource Type', 101 | name: 'resource_type', 102 | type: 'options', 103 | options: RESOURCE_TYPES.map((type) => ({ 104 | name: type, 105 | value: type, 106 | })), 107 | default: '', 108 | description: 'Filter by resource type (must be used together with Resource ID)', 109 | }, 110 | { 111 | displayName: 'Start Date', 112 | name: 'start_date', 113 | type: 'dateTime', 114 | default: '', 115 | description: 'Filter logs starting from this date (ISO 8601 format)', 116 | }, 117 | { 118 | displayName: 'User Email', 119 | name: 'user_email', 120 | type: 'string', 121 | default: '', 122 | description: 'Filter by exact user email match', 123 | }, 124 | { 125 | displayName: 'User Name or ID', 126 | name: 'user_id', 127 | type: 'options', 128 | typeOptions: { 129 | loadOptionsMethod: 'getUsers', 130 | }, 131 | default: '', 132 | description: 'Choose from the list, or specify an ID using an expression', 133 | }, 134 | ], 135 | }, 136 | 137 | // Fields for delete operation 138 | { 139 | displayName: 'Datetime', 140 | name: 'datetime', 141 | type: 'dateTime', 142 | required: true, 143 | default: '', 144 | description: 'Starting datetime from which logs will be deleted (ISO 8601 format)', 145 | displayOptions: { 146 | show: { 147 | resource: ['activity_logs'], 148 | operation: ['delete'], 149 | }, 150 | }, 151 | }, 152 | { 153 | displayName: 'Additional Fields', 154 | name: 'additionalFields', 155 | type: 'collection', 156 | placeholder: 'Add Field', 157 | default: {}, 158 | displayOptions: { 159 | show: { 160 | resource: ['activity_logs'], 161 | operation: ['delete'], 162 | }, 163 | }, 164 | options: [ 165 | { 166 | displayName: 'Delete Unassigned Logs Only', 167 | name: 'delete_unassigned_logs', 168 | type: 'boolean', 169 | default: false, 170 | description: 'Whether to only delete logs where user_id is nil', 171 | }, 172 | ], 173 | }, 174 | ]; 175 | -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/api_info.description.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const apiInfoOperations: INodeProperties[] = [ 4 | { 5 | displayName: 'Operation', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | displayOptions: { 10 | show: { 11 | resource: ['api_info'], 12 | }, 13 | }, 14 | options: [ 15 | { 16 | name: 'Get', 17 | value: 'get', 18 | description: 'Retrieve API information', 19 | action: 'Get API information', 20 | }, 21 | ], 22 | default: 'get', 23 | }, 24 | ]; 25 | 26 | export const apiInfoFields: INodeProperties[] = []; 27 | -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/cards.description.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { INTEGRATION_SLUGS } from '../utils/constants'; 3 | import { formatTitleCase } from '../utils/formatters'; 4 | 5 | export const cardsOperations: INodeProperties[] = [ 6 | { 7 | displayName: 'Operation', 8 | name: 'operation', 9 | type: 'options', 10 | noDataExpression: true, 11 | displayOptions: { 12 | show: { 13 | resource: ['cards'], 14 | }, 15 | }, 16 | options: [ 17 | { 18 | name: 'Lookup', 19 | value: 'lookup', 20 | description: 'Lookup cards with external integration details', 21 | action: 'Lookup cards with external integration details', 22 | }, 23 | { 24 | name: 'Jump', 25 | value: 'jump', 26 | description: 'Jump to a card by integration ID', 27 | action: 'Jump to a card by integration ID', 28 | }, 29 | { 30 | name: 'Jump By Identifier', 31 | value: 'jumpByIdentifier', 32 | description: 'Jump to a card by integration identifier', 33 | action: 'Jump to a card by integration identifier', 34 | }, 35 | ], 36 | default: 'lookup', 37 | }, 38 | ]; 39 | 40 | export const cardsFields: INodeProperties[] = [ 41 | // Fields for Lookup operation 42 | { 43 | displayName: 'Integration Slug', 44 | name: 'integration_slug', 45 | type: 'options', 46 | required: true, 47 | default: '', 48 | description: 'The integration type to use (e.g. autotask, cw_manage)', 49 | options: INTEGRATION_SLUGS.map((slug) => ({ 50 | name: formatTitleCase(slug), 51 | value: slug, 52 | })), 53 | displayOptions: { 54 | show: { 55 | resource: ['cards'], 56 | operation: ['lookup'], 57 | }, 58 | }, 59 | }, 60 | { 61 | displayName: 'Additional Fields', 62 | name: 'additionalFields', 63 | type: 'collection', 64 | placeholder: 'Add Field', 65 | default: {}, 66 | displayOptions: { 67 | show: { 68 | resource: ['cards'], 69 | operation: ['lookup'], 70 | }, 71 | }, 72 | options: [ 73 | { 74 | displayName: 'Integration ID', 75 | name: 'integration_id', 76 | type: 'string', 77 | default: '', 78 | description: 79 | 'ID in the external integration. Must be present unless Integration Identifier is set.', 80 | }, 81 | { 82 | displayName: 'Integration Identifier', 83 | name: 'integration_identifier', 84 | type: 'string', 85 | default: '', 86 | description: 'Identifier in the external integration (used if Integration ID is not set)', 87 | }, 88 | ], 89 | }, 90 | 91 | // Fields for Jump operation 92 | { 93 | displayName: 'Integration Slug', 94 | name: 'integration_slug', 95 | type: 'options', 96 | required: true, 97 | default: '', 98 | description: 'The integration type to use (e.g. autotask, cw_manage)', 99 | options: INTEGRATION_SLUGS.map((slug) => ({ 100 | name: formatTitleCase(slug), 101 | value: slug, 102 | })), 103 | displayOptions: { 104 | show: { 105 | resource: ['cards'], 106 | operation: ['jump'], 107 | }, 108 | }, 109 | }, 110 | { 111 | displayName: 'Integration ID', 112 | name: 'integration_id', 113 | type: 'string', 114 | required: true, 115 | default: '', 116 | description: 'The integration ID to use', 117 | displayOptions: { 118 | show: { 119 | resource: ['cards'], 120 | operation: ['jump'], 121 | }, 122 | }, 123 | }, 124 | { 125 | displayName: 'Integration Type', 126 | name: 'integration_type', 127 | type: 'string', 128 | required: true, 129 | default: '', 130 | description: 'The integration type to use', 131 | displayOptions: { 132 | show: { 133 | resource: ['cards'], 134 | operation: ['jump'], 135 | }, 136 | }, 137 | }, 138 | { 139 | displayName: 'Integration Slug', 140 | name: 'integration_slug', 141 | type: 'options', 142 | required: true, 143 | default: '', 144 | description: 'The integration type to use (e.g. autotask, cw_manage)', 145 | options: INTEGRATION_SLUGS.map((slug) => ({ 146 | name: formatTitleCase(slug), 147 | value: slug, 148 | })), 149 | displayOptions: { 150 | show: { 151 | resource: ['cards'], 152 | operation: ['jumpByIdentifier'], 153 | }, 154 | }, 155 | }, 156 | { 157 | displayName: 'Additional Fields', 158 | name: 'additionalFields', 159 | type: 'collection', 160 | placeholder: 'Add Field', 161 | default: {}, 162 | displayOptions: { 163 | show: { 164 | resource: ['cards'], 165 | operation: ['jump'], 166 | }, 167 | }, 168 | options: [ 169 | { 170 | displayName: 'Integration ID', 171 | name: 'integration_id', 172 | type: 'string', 173 | default: '', 174 | description: 'ID of the entity in the external integration', 175 | }, 176 | { 177 | displayName: 'Integration Identifier', 178 | name: 'integration_identifier', 179 | type: 'string', 180 | default: '', 181 | description: 182 | 'Identifier of the entity in the external integration (if integration_id is not set)', 183 | }, 184 | ], 185 | }, 186 | ]; 187 | -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/expirations.description.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { HUDU_API_CONSTANTS, RESOURCE_TYPES } from '../utils/constants'; 3 | 4 | export const expirationsOperations: INodeProperties[] = [ 5 | { 6 | displayName: 'Operation', 7 | name: 'operation', 8 | type: 'options', 9 | noDataExpression: true, 10 | displayOptions: { 11 | show: { 12 | resource: ['expirations'], 13 | }, 14 | }, 15 | options: [ 16 | { 17 | name: 'Get Many', 18 | value: 'getAll', 19 | description: 'Get many expirations', 20 | action: 'Get many expirations', 21 | }, 22 | ], 23 | default: 'getAll', 24 | }, 25 | ]; 26 | 27 | export const expirationsFields: INodeProperties[] = [ 28 | { 29 | displayName: 'Return All', 30 | name: 'returnAll', 31 | type: 'boolean', 32 | displayOptions: { 33 | show: { 34 | resource: ['expirations'], 35 | operation: ['getAll'], 36 | }, 37 | }, 38 | default: false, 39 | description: 'Whether to return all results or only up to a given limit', 40 | }, 41 | { 42 | displayName: 'Limit', 43 | name: 'limit', 44 | type: 'number', 45 | displayOptions: { 46 | show: { 47 | resource: ['expirations'], 48 | operation: ['getAll'], 49 | returnAll: [false], 50 | }, 51 | }, 52 | typeOptions: { 53 | minValue: 1, 54 | }, 55 | default: HUDU_API_CONSTANTS.PAGE_SIZE, 56 | description: 'Max number of results to return', 57 | }, 58 | { 59 | displayName: 'Filters', 60 | name: 'filters', 61 | type: 'collection', 62 | placeholder: 'Add Filter', 63 | default: {}, 64 | displayOptions: { 65 | show: { 66 | resource: ['expirations'], 67 | operation: ['getAll'], 68 | }, 69 | }, 70 | options: [ 71 | { 72 | displayName: 'Company Name or ID', 73 | name: 'company_id', 74 | type: 'options', 75 | typeOptions: { 76 | loadOptionsMethod: 'getCompanies', 77 | loadOptionsParameters: { 78 | includeBlank: true, 79 | }, 80 | }, 81 | default: '', 82 | description: 'Choose from the list, or specify an ID using an expression', 83 | }, 84 | { 85 | displayName: 'Expiration Type', 86 | name: 'expiration_type', 87 | type: 'options', 88 | options: [ 89 | { 90 | name: 'Article Expiration', 91 | value: 'article_expiration', 92 | }, 93 | { 94 | name: 'Asset Field', 95 | value: 'asset_field', 96 | }, 97 | { 98 | name: 'Domain', 99 | value: 'domain', 100 | }, 101 | { 102 | name: 'SSL Certificate', 103 | value: 'ssl_certificate', 104 | }, 105 | { 106 | name: 'Undeclared', 107 | value: 'undeclared', 108 | }, 109 | { 110 | name: 'Warranty', 111 | value: 'warranty', 112 | }, 113 | ], 114 | default: 'undeclared', 115 | description: 116 | 'Filter expirations by expiration type (undeclared, domain, ssl_certificate, warranty, asset_field, article_expiration)', 117 | }, 118 | { 119 | displayName: 'Resource ID', 120 | name: 'resource_id', 121 | type: 'number', 122 | default: undefined, 123 | description: 'Filter logs by resource ID; must be coupled with resource type', 124 | }, 125 | { 126 | displayName: 'Resource Type', 127 | name: 'resource_type', 128 | type: 'options', 129 | options: RESOURCE_TYPES.map((type) => ({ 130 | name: type, 131 | value: type, 132 | })), 133 | default: '', 134 | description: 'Filter logs by resource type; must be coupled with resource ID', 135 | }, 136 | ], 137 | }, 138 | ]; 139 | -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './activity_logs.description'; 2 | export * from './api_info.description'; 3 | export * from './articles.description'; 4 | export * from './asset_layouts.description'; 5 | export * from './asset_layout_fields.description'; 6 | export * from './asset_passwords.description'; 7 | export * from './assets.description'; 8 | export * from './HuduAssetLinkFieldDescription'; 9 | export * from './HuduAssetCustomFieldDescription'; 10 | export * from './HuduAssetStandardFieldDescription'; 11 | export * from './cards.description'; 12 | export * from './companies.description'; 13 | export * from './expirations.description'; 14 | export * from './folders.description'; 15 | export * from './ip_addresses.description'; 16 | export * from './list_options.description'; 17 | export * from './lists.description'; 18 | export * from './magic_dash.description'; 19 | export * from './matchers.description'; 20 | export * from './networks.description'; 21 | export * from './passwordFolders.descriptions'; 22 | export * from './procedures.description'; 23 | export * from './procedure_tasks.description'; 24 | export * from './public_photos.description'; 25 | export * from './rack_storages.description'; 26 | export * from './rack_storage_items.description'; 27 | export * from './relations.description'; 28 | export * from './uploads.description'; 29 | export * from './users.description'; 30 | export * from './websites.description'; 31 | export * from './vlans.description'; 32 | export * from './vlan_zones.description'; 33 | -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/list_options.description.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const listOptionsOperations: INodeProperties[] = [ 4 | { 5 | displayName: 'Operation', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | displayOptions: { 10 | show: { 11 | resource: ['list_options'], 12 | }, 13 | }, 14 | options: [ 15 | { 16 | name: 'Create', 17 | value: 'create', 18 | description: 'Create a new list item', 19 | action: 'Create a list item', 20 | }, 21 | { 22 | name: 'Delete', 23 | value: 'delete', 24 | description: 'Delete a list item', 25 | action: 'Delete a list item', 26 | }, 27 | { 28 | name: 'Get', 29 | value: 'get', 30 | description: 'Get list items for a specific list', 31 | action: 'Get list items', 32 | }, 33 | { 34 | name: 'Update', 35 | value: 'update', 36 | description: 'Update a list item', 37 | action: 'Update a list item', 38 | }, 39 | ], 40 | default: 'get', 41 | }, 42 | ]; 43 | 44 | export const listOptionsFields: INodeProperties[] = [ 45 | // List ID for all operations 46 | { 47 | displayName: 'List Name or ID', 48 | name: 'list_id', 49 | type: 'options', 50 | typeOptions: { 51 | loadOptionsMethod: 'getLists', 52 | }, 53 | required: true, 54 | displayOptions: { 55 | show: { 56 | resource: ['list_options'], 57 | operation: ['get', 'create', 'update', 'delete'], 58 | }, 59 | }, 60 | default: '', 61 | description: 'Choose from the list, or specify an ID using an expression', 62 | }, 63 | 64 | // Item ID for update and delete 65 | { 66 | displayName: 'List Item ID', 67 | name: 'item_id', 68 | type: 'string', 69 | required: true, 70 | displayOptions: { 71 | show: { 72 | resource: ['list_options'], 73 | operation: ['update', 'delete'], 74 | }, 75 | }, 76 | default: '', 77 | description: 'The ID of the list item', 78 | }, 79 | 80 | // Name for create and update 81 | { 82 | displayName: 'Name', 83 | name: 'name', 84 | type: 'string', 85 | required: true, 86 | displayOptions: { 87 | show: { 88 | resource: ['list_options'], 89 | operation: ['create', 'update'], 90 | }, 91 | }, 92 | default: '', 93 | description: 'The name of the list item', 94 | }, 95 | ]; -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/lists.description.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const listsOperations: INodeProperties[] = [ 4 | { 5 | displayName: 'Operation', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | displayOptions: { 10 | show: { 11 | resource: ['lists'], 12 | }, 13 | }, 14 | options: [ 15 | { 16 | name: 'Create', 17 | value: 'create', 18 | description: 'Create a new list', 19 | action: 'Create a list', 20 | }, 21 | { 22 | name: 'Delete', 23 | value: 'delete', 24 | description: 'Delete a list', 25 | action: 'Delete a list', 26 | }, 27 | { 28 | name: 'Get', 29 | value: 'get', 30 | description: 'Get a specific list', 31 | action: 'Get a list', 32 | }, 33 | { 34 | name: 'Get Many', 35 | value: 'getAll', 36 | description: 'Get many lists', 37 | action: 'Get many lists', 38 | }, 39 | { 40 | name: 'Update', 41 | value: 'update', 42 | description: 'Update a list', 43 | action: 'Update a list', 44 | }, 45 | ], 46 | default: 'getAll', 47 | }, 48 | ]; 49 | 50 | export const listsFields: INodeProperties[] = [ 51 | // List ID for get, update, delete 52 | { 53 | displayName: 'List Name or ID', 54 | name: 'id', 55 | type: 'options', 56 | typeOptions: { 57 | loadOptionsMethod: 'getLists', 58 | }, 59 | required: true, 60 | displayOptions: { 61 | show: { 62 | resource: ['lists'], 63 | operation: ['get', 'update', 'delete'], 64 | }, 65 | }, 66 | default: '', 67 | description: 'Choose from the list, or specify an ID using an expression', 68 | }, 69 | // Name for create 70 | { 71 | displayName: 'Name', 72 | name: 'name', 73 | type: 'string', 74 | required: true, 75 | displayOptions: { 76 | show: { 77 | resource: ['lists'], 78 | operation: ['create'], 79 | }, 80 | }, 81 | default: '', 82 | description: 'The name of the list', 83 | }, 84 | // Name for update 85 | { 86 | displayName: 'Name', 87 | name: 'updateFields.name', 88 | type: 'string', 89 | required: true, 90 | displayOptions: { 91 | show: { 92 | resource: ['lists'], 93 | operation: ['update'], 94 | }, 95 | }, 96 | default: '', 97 | description: 'The new name of the list', 98 | }, 99 | // Filters for getAll 100 | { 101 | displayName: 'Filters', 102 | name: 'filters', 103 | type: 'collection', 104 | placeholder: 'Add Filter', 105 | default: {}, 106 | displayOptions: { 107 | show: { 108 | resource: ['lists'], 109 | operation: ['getAll'], 110 | }, 111 | }, 112 | options: [ 113 | { 114 | displayName: 'Query', 115 | name: 'query', 116 | type: 'string', 117 | default: '', 118 | description: 'Search lists by name (partial match)', 119 | }, 120 | { 121 | displayName: 'Name', 122 | name: 'name', 123 | type: 'string', 124 | default: '', 125 | description: 'Filter by exact list name', 126 | }, 127 | ], 128 | description: 'Filters to apply when retrieving lists', 129 | }, 130 | // Return all toggle for getAll 131 | { 132 | displayName: 'Return All', 133 | name: 'returnAll', 134 | type: 'boolean', 135 | displayOptions: { 136 | show: { 137 | resource: ['lists'], 138 | operation: ['getAll'], 139 | }, 140 | }, 141 | default: false, 142 | description: 'Whether to return all results or limit by a set number', 143 | }, 144 | // Limit for getAll 145 | { 146 | displayName: 'Limit', 147 | name: 'limit', 148 | type: 'number', 149 | typeOptions: { 150 | minValue: 1, 151 | }, 152 | displayOptions: { 153 | show: { 154 | resource: ['lists'], 155 | operation: ['getAll'], 156 | returnAll: [false], 157 | }, 158 | }, 159 | default: 50, 160 | description: 'Max number of results to return', 161 | }, 162 | ]; -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/passwordFolders.descriptions.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const passwordFoldersOperations: INodeProperties[] = [ 4 | { 5 | displayName: 'Operation', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | displayOptions: { 10 | show: { 11 | resource: ['password_folders'], 12 | }, 13 | }, 14 | options: [ 15 | { 16 | name: 'Get', 17 | value: 'get', 18 | description: 'Get a password folder by ID', 19 | action: 'Get a password folder', 20 | }, 21 | { 22 | name: 'Get Many', 23 | value: 'getAll', 24 | description: 'Get many password folders', 25 | action: 'Get many password folders', 26 | }, 27 | ], 28 | default: 'getAll', 29 | }, 30 | ]; 31 | 32 | export const passwordFoldersFields: INodeProperties[] = [ 33 | { 34 | displayName: 'Password Folder ID', 35 | name: 'id', 36 | type: 'number', 37 | required: true, 38 | displayOptions: { 39 | show: { 40 | resource: ['password_folders'], 41 | operation: ['get'], 42 | }, 43 | }, 44 | default: 0, 45 | description: 'The ID of the password folder to retrieve', 46 | }, 47 | { 48 | displayName: 'Return All', 49 | name: 'returnAll', 50 | type: 'boolean', 51 | displayOptions: { 52 | show: { 53 | resource: ['password_folders'], 54 | operation: ['getAll'], 55 | }, 56 | }, 57 | default: false, 58 | description: 'Whether to return all results or only up to a given limit', 59 | }, 60 | { 61 | displayName: 'Limit', 62 | name: 'limit', 63 | type: 'number', 64 | displayOptions: { 65 | show: { 66 | resource: ['password_folders'], 67 | operation: ['getAll'], 68 | returnAll: [false], 69 | }, 70 | }, 71 | typeOptions: { 72 | minValue: 1, 73 | }, 74 | default: 50, 75 | description: 'Max number of results to return', 76 | }, 77 | { 78 | displayName: 'Filters', 79 | name: 'filters', 80 | type: 'collection', 81 | placeholder: 'Add Filter', 82 | default: {}, 83 | displayOptions: { 84 | show: { 85 | resource: ['password_folders'], 86 | operation: ['getAll'], 87 | }, 88 | }, 89 | options: [ 90 | { 91 | displayName: 'Company Name or ID', 92 | name: 'company_id', 93 | type: 'options', 94 | typeOptions: { 95 | loadOptionsMethod: 'getCompanies', 96 | loadOptionsParameters: { 97 | includeBlank: true, 98 | }, 99 | }, 100 | default: '', 101 | description: 'The company to associate with the password folder. Choose from the list, or specify an ID using an expression.', 102 | }, 103 | { 104 | displayName: 'Name', 105 | name: 'name', 106 | type: 'string', 107 | default: '', 108 | description: 'Filter folders by name', 109 | }, 110 | { 111 | displayName: 'Search', 112 | name: 'search', 113 | type: 'string', 114 | default: '', 115 | description: 'Filter by search query', 116 | }, 117 | ], 118 | }, 119 | ]; 120 | -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/resources.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const resourceOptions = [ 4 | { 5 | name: 'Activity Log', 6 | value: 'activity_logs', 7 | }, 8 | { 9 | name: 'API Info', 10 | value: 'api_info', 11 | }, 12 | { 13 | name: 'Article', 14 | value: 'articles', 15 | }, 16 | { 17 | name: 'Asset', 18 | value: 'assets', 19 | }, 20 | { 21 | name: 'Asset Field Custom', 22 | value: 'assetCustomField', 23 | }, 24 | { 25 | name: 'Asset Field Link', 26 | value: 'assetLinkField', 27 | }, 28 | { 29 | name: 'Asset Field Standard', 30 | value: 'assetStandardField', 31 | }, 32 | { 33 | name: 'Asset Layout', 34 | value: 'asset_layouts', 35 | }, 36 | { 37 | name: 'Asset Layout Field', 38 | value: 'asset_layout_fields', 39 | }, 40 | { 41 | name: 'Asset Password', 42 | value: 'asset_passwords', 43 | }, 44 | { 45 | name: 'Card', 46 | value: 'cards', 47 | }, 48 | { 49 | name: 'Company', 50 | value: 'companies', 51 | }, 52 | { 53 | name: 'Expiration', 54 | value: 'expirations', 55 | }, 56 | { 57 | name: 'Folder', 58 | value: 'folders', 59 | }, 60 | { 61 | name: 'IP Address', 62 | value: 'ipAddresses', 63 | }, 64 | { 65 | name: 'List', 66 | value: 'lists', 67 | }, 68 | { 69 | name: 'List Options', 70 | value: 'list_options', 71 | }, 72 | { 73 | name: 'Magic Dash', 74 | value: 'magic_dash', 75 | }, 76 | { 77 | name: 'Matcher', 78 | value: 'matchers', 79 | }, 80 | { 81 | name: 'Network', 82 | value: 'networks', 83 | }, 84 | { 85 | name: 'Password Folder', 86 | value: 'password_folders', 87 | }, 88 | { 89 | name: 'Procedure', 90 | value: 'procedures', 91 | }, 92 | { 93 | name: 'Procedure Task', 94 | value: 'procedure_tasks', 95 | }, 96 | { 97 | name: 'Public Photo', 98 | value: 'public_photos', 99 | }, 100 | { 101 | name: 'Rack Storage', 102 | value: 'rack_storages', 103 | }, 104 | { 105 | name: 'Rack Storage Item', 106 | value: 'rack_storage_items', 107 | }, 108 | { 109 | name: 'Relation', 110 | value: 'relations', 111 | }, 112 | { 113 | name: 'Upload', 114 | value: 'uploads', 115 | }, 116 | { 117 | name: 'User', 118 | value: 'users', 119 | }, 120 | { 121 | name: 'Website', 122 | value: 'websites', 123 | }, 124 | { 125 | name: 'VLAN', 126 | value: 'vlans', 127 | }, 128 | { 129 | name: 'VLAN Zone', 130 | value: 'vlan_zones', 131 | }, 132 | ]; 133 | 134 | export const resourceProperty: INodeProperties = { 135 | displayName: 'Resource', 136 | name: 'resource', 137 | type: 'options', 138 | noDataExpression: true, 139 | options: resourceOptions, 140 | default: 'companies', 141 | }; -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/uploads.description.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { HUDU_API_CONSTANTS } from '../utils/constants'; 3 | 4 | export const uploadsOperations: INodeProperties[] = [ 5 | { 6 | displayName: 'Operation', 7 | name: 'operation', 8 | type: 'options', 9 | noDataExpression: true, 10 | displayOptions: { 11 | show: { 12 | resource: ['uploads'], 13 | }, 14 | }, 15 | options: [ 16 | { 17 | name: 'Get Many', 18 | value: 'getAll', 19 | description: 'Get many uploads', 20 | action: 'Get many uploads', 21 | }, 22 | { 23 | name: 'Get', 24 | value: 'get', 25 | description: 'Get a specific upload', 26 | action: 'Get an upload', 27 | }, 28 | { 29 | name: 'Delete', 30 | value: 'delete', 31 | description: 'Delete an upload', 32 | action: 'Delete an upload', 33 | }, 34 | ], 35 | default: 'getAll', 36 | }, 37 | ]; 38 | 39 | export const uploadsFields: INodeProperties[] = [ 40 | // ---------------------------------- 41 | // getAll 42 | // ---------------------------------- 43 | { 44 | displayName: 'Return All', 45 | name: 'returnAll', 46 | type: 'boolean', 47 | displayOptions: { 48 | show: { 49 | resource: ['uploads'], 50 | operation: ['getAll'], 51 | }, 52 | }, 53 | default: false, 54 | description: 'Whether to return all results or only up to a given limit', 55 | }, 56 | { 57 | displayName: 'Limit', 58 | name: 'limit', 59 | type: 'number', 60 | displayOptions: { 61 | show: { 62 | resource: ['uploads'], 63 | operation: ['getAll'], 64 | returnAll: [false], 65 | }, 66 | }, 67 | typeOptions: { 68 | minValue: 1, 69 | }, 70 | default: HUDU_API_CONSTANTS.PAGE_SIZE, 71 | description: 'Max number of results to return', 72 | }, 73 | 74 | // ---------------------------------- 75 | // get/delete 76 | // ---------------------------------- 77 | { 78 | displayName: 'Upload ID', 79 | name: 'id', 80 | type: 'number', 81 | displayOptions: { 82 | show: { 83 | resource: ['uploads'], 84 | operation: ['get', 'delete'], 85 | }, 86 | }, 87 | default: 0, 88 | required: true, 89 | description: 'ID of the upload', 90 | }, 91 | ]; 92 | -------------------------------------------------------------------------------- /src/nodes/Hudu/descriptions/users.description.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { HUDU_API_CONSTANTS } from '../utils/constants'; 3 | 4 | export const userOperations: INodeProperties[] = [ 5 | { 6 | displayName: 'Operation', 7 | name: 'operation', 8 | type: 'options', 9 | noDataExpression: true, 10 | displayOptions: { 11 | show: { 12 | resource: ['users'], 13 | }, 14 | }, 15 | options: [ 16 | { 17 | name: 'Get Many', 18 | value: 'getAll', 19 | description: 'Get many users', 20 | action: 'Get many users', 21 | }, 22 | { 23 | name: 'Get', 24 | value: 'get', 25 | description: 'Get a user by ID', 26 | action: 'Get a user', 27 | }, 28 | ], 29 | default: 'getAll', 30 | }, 31 | ]; 32 | 33 | export const userFields: INodeProperties[] = [ 34 | // Return All option for GetAll operation 35 | { 36 | displayName: 'Return All', 37 | name: 'returnAll', 38 | type: 'boolean', 39 | displayOptions: { 40 | show: { 41 | resource: ['users'], 42 | operation: ['getAll'], 43 | }, 44 | }, 45 | default: false, 46 | description: 'Whether to return all results or only up to a given limit', 47 | }, 48 | { 49 | displayName: 'Limit', 50 | name: 'limit', 51 | type: 'number', 52 | displayOptions: { 53 | show: { 54 | resource: ['users'], 55 | operation: ['getAll'], 56 | returnAll: [false], 57 | }, 58 | }, 59 | typeOptions: { 60 | minValue: 1, 61 | }, 62 | default: HUDU_API_CONSTANTS.PAGE_SIZE, 63 | description: 'Max number of results to return', 64 | }, 65 | // ID field for Get operation 66 | { 67 | displayName: 'User ID', 68 | name: 'id', 69 | type: 'number', 70 | required: true, 71 | displayOptions: { 72 | show: { 73 | resource: ['users'], 74 | operation: ['get'], 75 | }, 76 | }, 77 | default: 0, 78 | description: 'ID of the user to retrieve', 79 | }, 80 | // Filters for GetAll operation 81 | { 82 | displayName: 'Filters', 83 | name: 'filters', 84 | type: 'collection', 85 | placeholder: 'Add Filter', 86 | default: {}, 87 | displayOptions: { 88 | show: { 89 | resource: ['users'], 90 | operation: ['getAll'], 91 | }, 92 | }, 93 | options: [ 94 | { 95 | displayName: 'Archived', 96 | name: 'archived', 97 | type: 'boolean', 98 | default: false, 99 | description: 'Whether the user is archived', 100 | }, 101 | { 102 | displayName: 'Email', 103 | name: 'email', 104 | type: 'string', 105 | placeholder: 'name@email.com', 106 | default: '', 107 | description: 'Filter users by email address', 108 | }, 109 | { 110 | displayName: 'First Name', 111 | name: 'first_name', 112 | type: 'string', 113 | default: '', 114 | description: 'Filter users by first name', 115 | }, 116 | { 117 | displayName: 'Last Name', 118 | name: 'last_name', 119 | type: 'string', 120 | default: '', 121 | description: 'Filter users by last name', 122 | }, 123 | { 124 | displayName: 'Portal Member Company Name or ID', 125 | name: 'portal_member_company_id', 126 | type: 'options', 127 | typeOptions: { 128 | loadOptionsMethod: 'getCompanies', 129 | loadOptionsParameters: { 130 | includeBlank: true, 131 | }, 132 | }, 133 | default: '', 134 | description: 'The company to associate with the portal member. Choose from the list, or specify an ID using an expression.', 135 | }, 136 | { 137 | displayName: 'Search', 138 | name: 'search', 139 | type: 'string', 140 | default: '', 141 | description: 'Search across first name and last name', 142 | }, 143 | { 144 | displayName: 'Security Level', 145 | name: 'security_level', 146 | type: 'options', 147 | options: [ 148 | { 149 | name: 'Admin', 150 | value: 'admin', 151 | }, 152 | { 153 | name: 'Author', 154 | value: 'author', 155 | }, 156 | { 157 | name: 'Editor', 158 | value: 'editor', 159 | }, 160 | { 161 | name: 'Portal Admin', 162 | value: 'portal_admin', 163 | }, 164 | { 165 | name: 'Portal Member', 166 | value: 'portal_member', 167 | }, 168 | { 169 | name: 'Spectator', 170 | value: 'spectator', 171 | }, 172 | { 173 | name: 'Super Admin', 174 | value: 'super_admin', 175 | }, 176 | ], 177 | default: 'super_admin', 178 | description: 'Filter users by security level', 179 | }, 180 | ], 181 | }, 182 | ]; 183 | -------------------------------------------------------------------------------- /src/nodes/Hudu/docs/FILTERS.md: -------------------------------------------------------------------------------- 1 | # Post-Processing Filters 2 | 3 | This document describes the post-processing filter system implemented for the Hudu node resources. 4 | 5 | ## Overview 6 | 7 | The filter system allows for flexible filtering of resource results after they are retrieved from the API. These filters are applied client-side after the API response is received. 8 | 9 | ## Resource-Specific Filters 10 | 11 | ### Articles 12 | - `folder_id`: Filter by folder ID (exact match) 13 | 14 | ### Folders 15 | - `parent_folder_id`: Filter by parent folder ID (exact match) 16 | - `childFolder`: Filter by whether the folder is a child ('yes'/'no') 17 | 18 | ### Relations 19 | - `fromable_type`: Filter by the type of the origin entity (case-insensitive match) 20 | - `fromable_id`: Filter by the ID of the origin entity (exact match) 21 | - `toable_type`: Filter by the type of the destination entity (case-insensitive match) 22 | - `toable_id`: Filter by the ID of the destination entity (exact match) 23 | - `is_inverse`: Filter by whether the relation is inverse (boolean match) 24 | 25 | ## Implementation Details 26 | 27 | Post-processing filters are implemented using the `FilterMapping` type and applied using the `applyPostFilters` utility function. These filters are executed client-side after retrieving data from the API. 28 | 29 | ## Best Practices 30 | 31 | 1. Use post-processing filters sparingly as they may impact performance 32 | 2. Consider case sensitivity requirements for string matches 33 | 3. Use appropriate type conversion for numeric comparisons 34 | 4. Document any special filter behavior in the code 35 | 36 | ## Note 37 | 38 | Other filters mentioned in the resource descriptions are applied directly through the API and are not part of the post-processing filter system. -------------------------------------------------------------------------------- /src/nodes/Hudu/hudu.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.16, written by Peter Selinger 2001-2019 9 | 10 | 12 | 15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/asset_layouts/getAssetLayoutFieldValues.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; 2 | import { handleGetOperation } from '../../utils/operations'; 3 | import type { IAssetLayoutFieldEntity } from '../../resources/asset_layout_fields/asset_layout_fields.types'; 4 | 5 | export async function getAssetLayoutFieldValues( 6 | this: ILoadOptionsFunctions, 7 | ): Promise { 8 | try { 9 | const layoutId = this.getNodeParameter('asset_layout_id') as string; 10 | const fieldId = this.getNodeParameter('field_id') as string; 11 | 12 | if (!layoutId || !fieldId) { 13 | return []; 14 | } 15 | 16 | // Fetch the layout details 17 | const layout = await handleGetOperation.call(this, '/asset_layouts', layoutId); 18 | const fields = (layout as { asset_layout: { fields: IAssetLayoutFieldEntity[] } }).asset_layout.fields || []; 19 | const field = fields.find((f) => f.id === Number.parseInt(fieldId, 10)); 20 | 21 | if (!field) { 22 | return []; 23 | } 24 | 25 | // Return the current values of the field as options 26 | return [ 27 | { 28 | name: 'Expiration', 29 | value: 'expiration', 30 | description: field.expiration.toString(), 31 | }, 32 | { 33 | name: 'Field Type', 34 | value: 'field_type', 35 | description: field.field_type, 36 | }, 37 | { 38 | name: 'Hint', 39 | value: 'hint', 40 | description: field.hint, 41 | }, 42 | { 43 | name: 'Label', 44 | value: 'label', 45 | description: field.label, 46 | }, 47 | { 48 | name: 'Options', 49 | value: 'options', 50 | description: field.options, 51 | }, 52 | { 53 | name: 'Position', 54 | value: 'position', 55 | description: field.position.toString(), 56 | }, 57 | { 58 | name: 'Required', 59 | value: 'required', 60 | description: field.required?.toString() || 'false', 61 | }, 62 | { 63 | name: 'Show in List', 64 | value: 'show_in_list', 65 | description: field.show_in_list.toString(), 66 | }, 67 | ]; 68 | } catch (error) { 69 | return []; 70 | } 71 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/asset_layouts/getAssetLayouts.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, IDataObject, INodePropertyOptions } from 'n8n-workflow'; 2 | import { handleListing } from '../../utils'; 3 | 4 | interface AssetLayoutOption { 5 | name: string; 6 | value: number; 7 | archived: boolean; 8 | } 9 | 10 | interface HuduAssetLayout extends IDataObject { 11 | id: number; 12 | name: string; 13 | active: boolean; 14 | } 15 | 16 | /** 17 | * Get all asset layouts from Hudu API 18 | */ 19 | export async function getAssetLayouts(this: ILoadOptionsFunctions): Promise { 20 | try { 21 | let includeBlank = false; 22 | try { 23 | includeBlank = (this.getNodeParameter('parameters.includeBlank', false) as boolean) || false; 24 | } catch { 25 | // Parameter doesn't exist, ignore 26 | } 27 | 28 | const assetLayouts = (await handleListing.call( 29 | this, 30 | 'GET', 31 | '/asset_layouts', 32 | 'asset_layouts', 33 | {}, 34 | {}, 35 | true, 36 | 0, 37 | )) as HuduAssetLayout[]; 38 | 39 | const mappedLayouts = assetLayouts 40 | .map((layout) => ({ 41 | name: `${layout.name} (${layout.id})${!layout.active ? ' - Archived' : ''}`, 42 | value: layout.id, 43 | archived: !layout.active, 44 | })) 45 | .sort((a: AssetLayoutOption, b: AssetLayoutOption) => { 46 | if (a.archived !== b.archived) { 47 | return a.archived ? 1 : -1; 48 | } 49 | return a.name.localeCompare(b.name); 50 | }); 51 | 52 | if (includeBlank) { 53 | return [ 54 | { 55 | name: '- No Asset Layout -', 56 | value: '', 57 | }, 58 | ...mappedLayouts.map(({ name, value }) => ({ name, value })), 59 | ]; 60 | } 61 | 62 | return mappedLayouts.map(({ name, value }) => ({ name, value })); 63 | } catch (error) { 64 | return []; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/asset_layouts/getAssetLinkFields.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../../utils/requestUtils'; 3 | import { debugLog } from '../../utils/debugConfig'; 4 | import { handleGetOperation } from '../../utils/operations'; 5 | import type { IAssetLayoutFieldEntity } from '../../resources/asset_layout_fields/asset_layout_fields.types'; 6 | import { ASSET_LAYOUT_FIELD_TYPES } from '../../utils/constants'; 7 | import type { IAssetResponse } from '../../resources/assets/assets.types'; 8 | 9 | /** 10 | * Loads asset link fields (fields of type AssetTag) for a given asset. 11 | * Used as a loadOptionsMethod for the assetLinkField resource dropdown in n8n. 12 | * 13 | * @param this - n8n ILoadOptionsFunctions context 14 | * @returns Array of options for asset link fields 15 | */ 16 | export async function getAssetLinkFields( 17 | this: ILoadOptionsFunctions, 18 | ): Promise<{ name: string; value: string; description?: string }[]> { 19 | const assetId = this.getCurrentNodeParameter('assetId') as string | number; 20 | if (!assetId || isNaN(Number(assetId)) || Number(assetId) <= 0) { 21 | return [{ name: 'Please Enter a Valid Asset ID First', value: '' }]; 22 | } 23 | 24 | try { 25 | debugLog('[OPTION_LOADING] Fetching asset link fields for asset ID:', assetId); 26 | 27 | // Fetch the asset to get its layout ID 28 | const assetResponse = await huduApiRequest.call(this, 'GET', '/assets', {}, { id: assetId }) as IAssetResponse; 29 | if (!assetResponse || !assetResponse.assets || !assetResponse.assets.length) { 30 | debugLog('[OPTION_LOADING] No asset found with ID:', assetId); 31 | return [{ name: `Asset with ID ${assetId} Not Found`, value: '' }]; 32 | } 33 | 34 | const asset = assetResponse.assets[0]; 35 | const layoutId = asset.asset_layout_id; 36 | if (!layoutId) { 37 | debugLog('[OPTION_LOADING] Asset has no layout ID'); 38 | return [{ name: 'Asset Has No Associated Layout', value: '' }]; 39 | } 40 | 41 | // Fetch the layout 42 | debugLog('[OPTION_LOADING] Fetching layout details for ID:', layoutId); 43 | const layoutResponse = await handleGetOperation.call(this, '/asset_layouts', layoutId); 44 | const layout = (layoutResponse as { asset_layout: { fields: IAssetLayoutFieldEntity[] } }).asset_layout; 45 | if (!layout || !Array.isArray(layout.fields)) { 46 | debugLog('[OPTION_LOADING] Layout not found or has no fields'); 47 | return [{ name: 'Layout Not Found or Has No Fields', value: '' }]; 48 | } 49 | 50 | // Filter for ASSET_TAG fields 51 | const assetTagFields = layout.fields.filter((field: IAssetLayoutFieldEntity) => field.field_type === ASSET_LAYOUT_FIELD_TYPES.ASSET_TAG && !field.is_destroyed); 52 | debugLog('[OPTION_LOADING] Found asset link fields:', { count: assetTagFields.length }); 53 | 54 | // Return message if no asset link fields are found 55 | if (assetTagFields.length === 0) { 56 | debugLog('[OPTION_LOADING] No asset link fields found in layout'); 57 | return [{ name: 'No Asset Link Fields Found for This Asset', value: 'NO_LINK_FIELDS_FOUND' }]; 58 | } 59 | 60 | // Return as options 61 | return assetTagFields.map(field => ({ 62 | name: field.label, 63 | value: field.label, 64 | description: field.hint || undefined, 65 | })); 66 | } catch (error) { 67 | debugLog('[OPTION_LOADING] Error in getAssetLinkFields:', error); 68 | return [{ name: `Error: ${(error as Error).message}`, value: '' }]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/asset_layouts/getCustomFieldsLayoutFields.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; 2 | import { NodeOperationError } from 'n8n-workflow'; 3 | import { handleGetOperation } from '../../utils/operations'; 4 | import type { IAssetLayoutFieldEntity } from '../../resources/asset_layout_fields/asset_layout_fields.types'; 5 | import { debugLog } from '../../utils/debugConfig'; 6 | import { ASSET_LAYOUT_FIELD_TYPES } from '../../utils/constants'; 7 | 8 | interface IAssetLayout { 9 | active: boolean; 10 | name: string; 11 | fields: IAssetLayoutFieldEntity[]; 12 | } 13 | 14 | interface IAssetLayoutResponse { 15 | asset_layout: IAssetLayout; 16 | } 17 | 18 | export async function getCustomFieldsLayoutFields( 19 | this: ILoadOptionsFunctions, 20 | ): Promise { 21 | try { 22 | debugLog('[ASSET_OPTIONS] Starting getCustomFieldsLayoutFields'); 23 | 24 | const layoutId = this.getCurrentNodeParameter('getall_asset_layout_id') as string; 25 | 26 | debugLog('[ASSET_OPTIONS] Context:', { 27 | node: this.getNode().name, 28 | layoutId, 29 | }); 30 | 31 | if (!layoutId) { 32 | debugLog('[ASSET_OPTIONS] No layout ID provided, returning empty options'); 33 | return []; 34 | } 35 | 36 | // Fetch the layout details 37 | debugLog('[ASSET_OPTIONS] Fetching layout details for ID:', layoutId); 38 | const response = await handleGetOperation.call(this, '/asset_layouts', layoutId); 39 | const layout = response as unknown as IAssetLayoutResponse; 40 | debugLog('[ASSET_OPTIONS] Layout response:', layout); 41 | 42 | // Check if layout exists 43 | if (!layout || !layout.asset_layout) { 44 | debugLog('[ASSET_OPTIONS] Layout not found or inaccessible'); 45 | throw new NodeOperationError(this.getNode(), 'Asset layout not found or inaccessible'); 46 | } 47 | 48 | const layoutData = layout.asset_layout; 49 | const fields = layoutData.fields || []; 50 | debugLog('[ASSET_OPTIONS] Found fields:', fields); 51 | 52 | // If no fields are found 53 | if (fields.length === 0) { 54 | debugLog('[ASSET_OPTIONS] No fields found in layout, returning empty options'); 55 | return []; 56 | } 57 | 58 | // Map fields to options format 59 | debugLog('[ASSET_OPTIONS] Starting field mapping process'); 60 | const options = fields 61 | .filter((field: IAssetLayoutFieldEntity) => !field.is_destroyed && field.field_type === ASSET_LAYOUT_FIELD_TYPES.ASSET_TAG) 62 | .map((field: IAssetLayoutFieldEntity) => ({ 63 | name: `${field.label} (${field.field_type})${!layoutData.active ? ' [Archived Layout]' : ''}`, 64 | value: field.id.toString(), 65 | description: field.hint || undefined, 66 | })) 67 | .sort((a, b) => a.name.localeCompare(b.name)); 68 | 69 | debugLog('[ASSET_OPTIONS] Final mapped options:', options); 70 | return options; 71 | } catch (error) { 72 | debugLog('[ASSET_OPTIONS] Error in getCustomFieldsLayoutFields:', error); 73 | if (error instanceof NodeOperationError) { 74 | throw error; 75 | } 76 | throw new NodeOperationError(this.getNode(), `Failed to load custom fields layout fields: ${(error as Error).message}`); 77 | } 78 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/asset_layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getAssetLayouts'; 2 | export * from './getAssetLayoutFields'; 3 | export * from './getCustomFieldsLayoutFields'; 4 | export * from './getAssetLayoutFieldValues'; 5 | export * from './getAssetLinkFields'; 6 | -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/assets/getAssetCustomFields.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, INodePropertyOptions, IDataObject } from 'n8n-workflow'; 2 | import { NodeOperationError } from 'n8n-workflow'; 3 | import { huduApiRequest } from '../../utils/requestUtils'; 4 | import { debugLog } from '../../utils/debugConfig'; 5 | 6 | export async function getAssetCustomFields( 7 | this: ILoadOptionsFunctions, 8 | ): Promise { 9 | const assetId = this.getCurrentNodeParameter('assetId') as string | number; 10 | if (!assetId || isNaN(Number(assetId)) || Number(assetId) <= 0) { 11 | return [{ name: 'Please Enter a Valid Asset ID First', value: '' }]; 12 | } 13 | 14 | try { 15 | debugLog('[OPTION_LOADING] Fetching custom fields for asset ID:', assetId); 16 | const response = await huduApiRequest.call(this, 'GET', '/assets', {}, { id: assetId }) as IDataObject; 17 | const assets = Array.isArray(response.assets) ? response.assets : []; 18 | 19 | if (!assets.length) { 20 | debugLog('[OPTION_LOADING] No asset found with ID:', assetId); 21 | return [{ name: `Asset with ID ${assetId} not found`, value: '' }]; 22 | } 23 | 24 | const fields = Array.isArray(assets[0].fields) ? assets[0].fields : []; 25 | debugLog('[OPTION_LOADING] Found fields for asset:', { count: fields.length }); 26 | debugLog('[OPTION_LOADING] Field labels and types (raw):', fields.map((f: any) => ({ label: f.label, type: f.field_type }))); 27 | 28 | // Fetch asset layout to get field types 29 | const assetLayoutId = assets[0].asset_layout_id; 30 | let layoutFields: any[] = []; 31 | if (assetLayoutId) { 32 | try { 33 | const layoutResponse = await huduApiRequest.call(this, 'GET', `/asset_layouts/${assetLayoutId}`) as IDataObject; 34 | layoutFields = Array.isArray((layoutResponse.asset_layout as IDataObject)?.fields) 35 | ? (layoutResponse.asset_layout as IDataObject).fields as IDataObject[] 36 | : []; 37 | debugLog('[OPTION_LOADING] Asset layout fields retrieved', { count: layoutFields.length }); 38 | } catch (layoutError) { 39 | debugLog('[OPTION_LOADING] Error fetching asset layout:', layoutError); 40 | } 41 | } 42 | 43 | // Build picklist from layout fields, not asset fields 44 | if (!layoutFields.length) { 45 | debugLog('[OPTION_LOADING] No layout fields found, cannot build picklist'); 46 | return [{ name: 'No Custom Fields Found for This Asset', value: 'NO_CUSTOM_FIELDS_FOUND' }]; 47 | } 48 | 49 | // Map asset field values by label for quick lookup 50 | const assetFieldValueMap = new Map(); 51 | for (const field of fields) { 52 | if (field.label) { 53 | assetFieldValueMap.set(field.label, field.value); 54 | } 55 | } 56 | 57 | // Build picklist from layout fields 58 | const customFields = layoutFields 59 | .filter((layoutField: any) => layoutField.field_type !== 'AssetTag') 60 | .map((layoutField: any) => { 61 | const value = assetFieldValueMap.get(layoutField.label); 62 | return { 63 | name: layoutField.label, 64 | value: layoutField.label, 65 | description: layoutField.field_type + (value !== undefined && value !== null ? ` (Current value: ${value})` : ''), 66 | }; 67 | }); 68 | debugLog('[OPTION_LOADING] Custom fields from layout:', customFields); 69 | 70 | if (customFields.length === 0) { 71 | debugLog('[OPTION_LOADING] No custom fields found for asset/layout'); 72 | return [{ name: 'No Custom Fields Found for This Asset', value: 'NO_CUSTOM_FIELDS_FOUND' }]; 73 | } 74 | 75 | return customFields; 76 | } catch (error) { 77 | debugLog('[OPTION_LOADING] Error in getAssetCustomFields:', error); 78 | throw new NodeOperationError(this.getNode(), `Failed to load custom fields for asset: ${(error as Error).message}`); 79 | } 80 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/assets/getAssets.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleListing } from '../../utils'; 3 | 4 | interface AssetOption { 5 | name: string; 6 | value: string | number; 7 | } 8 | 9 | interface HuduAsset extends IDataObject { 10 | id: number; 11 | name: string; 12 | asset_type: string; 13 | company_name: string; 14 | slug: string; 15 | archived: boolean; 16 | company_id: number; 17 | asset_layout_id: number; 18 | } 19 | 20 | /** 21 | * Format asset display name based on filtering context 22 | */ 23 | function formatAssetName( 24 | asset: HuduAsset, 25 | showAssetType: boolean, 26 | showCompanyName: boolean, 27 | ): string { 28 | const parts = [asset.name]; 29 | 30 | if (showAssetType) { 31 | parts.push(`(${asset.asset_type})`); 32 | } 33 | 34 | if (showCompanyName) { 35 | parts.push(`(${asset.company_name})`); 36 | } 37 | 38 | return parts.join(' '); 39 | } 40 | 41 | /** 42 | * Get assets from Hudu API with optional filtering and grouping 43 | */ 44 | export async function getAssets(this: ILoadOptionsFunctions) { 45 | try { 46 | const companyId = this.getNodeParameter('company_id', 0) as string; 47 | const assetLayoutId = this.getNodeParameter('asset_layout_id', 0) as string; 48 | 49 | // Build query parameters 50 | const qs: IDataObject = { 51 | archived: false, 52 | }; 53 | 54 | if (companyId) { 55 | qs.company_id = Number.parseInt(companyId, 10); 56 | } 57 | 58 | if (assetLayoutId) { 59 | qs.asset_layout_id = Number.parseInt(assetLayoutId, 10); 60 | } 61 | 62 | const assets = (await handleListing.call( 63 | this, 64 | 'GET', 65 | '/assets', 66 | 'assets', 67 | qs, 68 | {}, 69 | true, 70 | 0, 71 | )) as HuduAsset[]; 72 | 73 | if (!Array.isArray(assets)) { 74 | return []; 75 | } 76 | 77 | // Determine display options based on filters 78 | const showAssetType = !assetLayoutId; 79 | const showCompanyName = !companyId; 80 | 81 | // Map assets to options with appropriate grouping 82 | if (showAssetType) { 83 | // Group by asset_type (primary grouping) 84 | const groupedByType = assets.reduce((acc, asset) => { 85 | const group = asset.asset_type; 86 | if (!acc[group]) { 87 | acc[group] = []; 88 | } 89 | acc[group].push({ 90 | name: formatAssetName(asset, false, showCompanyName), 91 | value: JSON.stringify({ 92 | id: asset.id, 93 | url: `/a/${asset.slug}`, 94 | name: asset.name 95 | }), 96 | }); 97 | return acc; 98 | }, {} as { [key: string]: AssetOption[] }); 99 | 100 | // Sort groups and items within groups 101 | return Object.entries(groupedByType) 102 | .sort(([a], [b]) => a.localeCompare(b)) 103 | .flatMap(([group, options]) => [ 104 | { name: group, value: group }, 105 | ...options.sort((a, b) => a.name.localeCompare(b.name)), 106 | ]); 107 | } 108 | 109 | // Group by company if not grouping by asset_type 110 | const groupedByCompany = assets.reduce((acc, asset) => { 111 | const group = asset.company_name; 112 | if (!acc[group]) { 113 | acc[group] = []; 114 | } 115 | acc[group].push({ 116 | name: formatAssetName(asset, showAssetType, false), 117 | value: JSON.stringify({ 118 | id: asset.id, 119 | url: `/a/${asset.slug}`, 120 | name: asset.name 121 | }), 122 | }); 123 | return acc; 124 | }, {} as { [key: string]: AssetOption[] }); 125 | 126 | // Sort groups and items within groups 127 | return Object.entries(groupedByCompany) 128 | .sort(([a], [b]) => a.localeCompare(b)) 129 | .flatMap(([group, options]) => [ 130 | { name: group, value: group }, 131 | ...options.sort((a, b) => a.name.localeCompare(b.name)), 132 | ]); 133 | } catch (error) { 134 | return []; 135 | } 136 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/assets/getAssetsForCompany.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; 2 | import { debugLog } from '../../utils/debugConfig'; 3 | import { handleListing } from '../../utils'; 4 | 5 | export async function getAssetsForCompany(this: ILoadOptionsFunctions): Promise { 6 | const companyId = this.getCurrentNodeParameter('companyIdForAssetLookup') as string | number; 7 | 8 | debugLog('[OPTION_LOADING] getAssetsForCompany called', { companyId }); 9 | 10 | if (!companyId || companyId === '') { 11 | debugLog('[OPTION_LOADING] No companyId provided for getAssetsForCompany'); 12 | return [{ name: 'Select a Company first...', value: '' }]; 13 | } 14 | 15 | try { 16 | const endpoint = `/companies/${companyId}/assets`; 17 | debugLog('[OPTION_LOADING] Fetching assets for company using handleListing', { endpoint }); 18 | 19 | // Use handleListing to properly handle pagination 20 | const assets = await handleListing.call( 21 | this, 22 | 'GET', 23 | endpoint, 24 | 'assets', // resourceName 25 | {}, // body 26 | {}, // query parameters 27 | true, // returnAll 28 | 0, // limit 29 | ); 30 | 31 | debugLog('[OPTION_LOADING] Received assets for company', { count: assets.length }); 32 | 33 | if (Array.isArray(assets) && assets.length > 0) { 34 | const mappedAssets = assets.map((asset: any) => ({ 35 | name: asset.name || `ID: ${asset.id}`, 36 | value: asset.id, 37 | })); 38 | 39 | debugLog('[OPTION_LOADING] Mapped assets for options', { count: mappedAssets.length }); 40 | 41 | if (mappedAssets.length === 0) { 42 | return [{ name: 'No Assets Found for This Company', value: '' }]; 43 | } 44 | 45 | return mappedAssets; 46 | } else { 47 | debugLog('[OPTION_LOADING] No assets found or invalid format', { assets }); 48 | return [{ name: 'No Assets Found for This Company', value: '' }]; 49 | } 50 | } catch (error) { 51 | debugLog('[ERROR] Error loading assets for company', { error }); 52 | return [ 53 | { 54 | name: `Error: ${error instanceof Error ? error.message : 'Failed to load assets'}`, 55 | value: 'ERROR_LOADING_ASSETS', 56 | }, 57 | ]; 58 | } 59 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/assets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getAssets'; 2 | export * from './getAssetCustomFields'; 3 | export * from './getLinkableAssets'; -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/companies/getCompanies.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleListing } from '../../utils'; 3 | 4 | interface CompanyOption { 5 | name: string; 6 | value: number; 7 | } 8 | 9 | interface HuduCompany extends IDataObject { 10 | id: number; 11 | name: string; 12 | } 13 | 14 | /** 15 | * Get all companies from Hudu API 16 | */ 17 | export async function getCompanies(this: ILoadOptionsFunctions) { 18 | try { 19 | const includeBlank = this.getNodeParameter('includeBlank', true) as boolean; 20 | 21 | const companies = (await handleListing.call( 22 | this, 23 | 'GET', 24 | '/companies', 25 | 'companies', 26 | {}, 27 | {}, 28 | true, 29 | 0, 30 | )) as HuduCompany[]; 31 | 32 | if (!Array.isArray(companies)) { 33 | return []; 34 | } 35 | 36 | const mappedCompanies = companies 37 | .map((company) => ({ 38 | name: `${company.name} (${company.id})`, 39 | value: company.id, 40 | })) 41 | .sort((a: CompanyOption, b: CompanyOption) => a.name.localeCompare(b.name)); 42 | 43 | if (includeBlank) { 44 | return [ 45 | { 46 | name: '- No Company -', 47 | value: '', 48 | }, 49 | ...mappedCompanies, 50 | ]; 51 | } 52 | 53 | return mappedCompanies; 54 | } catch (error) { 55 | return []; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/companies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getCompanies'; 2 | -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './users'; 2 | export * from './companies'; 3 | export * from './asset_layouts'; 4 | export * from './lists'; 5 | -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/lists/getLists.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, IDataObject, INodePropertyOptions } from 'n8n-workflow'; 2 | import { handleListing } from '../../utils'; 3 | 4 | interface IList extends IDataObject { 5 | id: number; 6 | name: string; 7 | list_items: { 8 | id: number; 9 | name: string; 10 | }[]; 11 | } 12 | 13 | /** 14 | * Get all lists from Hudu API 15 | */ 16 | export async function getLists(this: ILoadOptionsFunctions): Promise { 17 | try { 18 | // Use debug to see what's happening 19 | const lists = (await handleListing.call( 20 | this, 21 | 'GET', 22 | '/lists', 23 | 'lists', 24 | {}, 25 | {}, 26 | true, 27 | 0, 28 | )) as IList[]; 29 | 30 | if (!Array.isArray(lists)) { 31 | console.log('getLists returned non-array:', lists); 32 | return []; 33 | } 34 | 35 | console.log(`getLists found ${lists.length} lists`); 36 | 37 | const mappedLists = lists 38 | .map((list) => { 39 | // Ensure ID is a number and convert to string for the value 40 | const id = typeof list.id === 'number' ? list.id : parseInt(String(list.id), 10); 41 | return { 42 | name: `${list.name as string} (${id})`, 43 | value: id, 44 | }; 45 | }) 46 | .sort((a, b) => a.name.localeCompare(b.name)); 47 | 48 | // Log the first few mapped lists 49 | console.log('First few mapped lists:', mappedLists.slice(0, 3)); 50 | 51 | return mappedLists as INodePropertyOptions[]; 52 | } catch (error) { 53 | console.error('Error in getLists:', error); 54 | return []; 55 | } 56 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/lists/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getLists'; -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/users/getUsers.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadOptionsFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleListing } from '../../utils'; 3 | 4 | interface UserOption { 5 | name: string; 6 | value: number; 7 | archived: boolean; 8 | } 9 | 10 | interface HuduUser extends IDataObject { 11 | id: number; 12 | first_name: string; 13 | last_name: string; 14 | archived: boolean; 15 | } 16 | 17 | /** 18 | * Get all users from Hudu API 19 | */ 20 | export async function getUsers(this: ILoadOptionsFunctions) { 21 | try { 22 | const includeBlank = this.getNodeParameter('includeBlank', true) as boolean; 23 | const users = (await handleListing.call( 24 | this, 25 | 'GET', 26 | '/users', 27 | 'users', 28 | {}, 29 | {}, 30 | true, // returnAll 31 | 0, // no limit 32 | )) as HuduUser[]; 33 | 34 | if (!Array.isArray(users)) { 35 | return []; 36 | } 37 | 38 | const mappedUsers = users 39 | .map((user) => ({ 40 | name: `${user.first_name} ${user.last_name} (${user.id})${ 41 | user.archived ? ' - Archived' : '' 42 | }`, 43 | value: user.id, 44 | archived: user.archived, 45 | })) 46 | .sort((a: UserOption, b: UserOption) => { 47 | if (a.archived !== b.archived) { 48 | return a.archived ? 1 : -1; 49 | } 50 | return a.name.localeCompare(b.name); 51 | }); 52 | 53 | if (includeBlank) { 54 | return [ 55 | { 56 | name: '- No User -', 57 | value: '', 58 | }, 59 | ...mappedUsers.map(({ name, value }) => ({ name, value })), 60 | ]; 61 | } 62 | 63 | return mappedUsers.map(({ name, value }) => ({ name, value })); 64 | } catch (error) { 65 | return []; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/nodes/Hudu/optionLoaders/users/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getUsers'; 2 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/activity_logs/activity_logs.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; 2 | import { handleGetAllOperation } from '../../utils/operations'; 3 | import type { ActivityLogsOperation } from './activity_logs.types'; 4 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 5 | 6 | export async function handleActivityLogsOperation( 7 | this: IExecuteFunctions, 8 | operation: ActivityLogsOperation, 9 | i: number, 10 | ): Promise { 11 | const resourceEndpoint = '/activity_logs'; 12 | 13 | switch (operation) { 14 | case 'getAll': { 15 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 16 | const filters = this.getNodeParameter('additionalFields', i) as IDataObject; 17 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 18 | 19 | return await handleGetAllOperation.call( 20 | this, 21 | resourceEndpoint, 22 | 'activity_logs', 23 | filters, 24 | returnAll, 25 | limit, 26 | ); 27 | } 28 | } 29 | 30 | throw new Error(`Unsupported operation ${operation}`); 31 | } 32 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/activity_logs/activity_logs.types.ts: -------------------------------------------------------------------------------- 1 | export type ActivityLogsOperation = 'getAll' | 'delete'; 2 | 3 | export interface IActivityLogsGetAllParams { 4 | /** n8n UI parameter for maximum number of records to return */ 5 | limit?: number; 6 | /** Specify the current page of results to retrieve */ 7 | page?: number; 8 | /** Specify the number of results to return per page */ 9 | page_size?: number; 10 | /** Filter logs by a specific user ID */ 11 | user_id?: number; 12 | /** Filter logs by a user's email address */ 13 | user_email?: string; 14 | /** Filter logs by resource ID (matches response's record_id); must be used in conjunction with resource_type */ 15 | resource_id?: number; 16 | /** Filter logs by resource type (matches response's record_type - Asset, AssetPassword, Company, Article, etc.); must be used in conjunction with resource_id */ 17 | resource_type?: string; 18 | /** Filter logs by the action performed (matches response's action field) */ 19 | action_message?: string; 20 | /** Filter logs starting from a specific date; must be in ISO 8601 format */ 21 | start_date?: string; 22 | } 23 | 24 | export interface IActivityLogsDeleteParams { 25 | /** Starting datetime from which logs will be deleted (ISO 8601 format) */ 26 | datetime: string; 27 | /** Whether to only delete logs where user_id is nil */ 28 | delete_unassigned_logs?: boolean; 29 | } 30 | 31 | /** 32 | * Activity Log response from the API 33 | * Note: When filtering, resource_id maps to record_id and resource_type maps to record_type in the response 34 | */ 35 | export interface IActivityLog { 36 | /** Unique identifier for the activity log */ 37 | id: number; 38 | /** Additional details about the activity, may contain JSON data */ 39 | details: Record; 40 | /** The action performed (maps to action_message in query) */ 41 | action: string; 42 | /** IP address from which the action was performed */ 43 | ip_address: string; 44 | /** Unique token for the activity */ 45 | token: string; 46 | /** ID of the user who performed the action */ 47 | user_id: number | null; 48 | /** Email of the user who performed the action */ 49 | user_email: string | null; 50 | /** Original name of the record if it was changed */ 51 | original_record_name: string | null; 52 | /** Full name of the user who performed the action */ 53 | user_name: string; 54 | /** Short name of the user who performed the action */ 55 | user_short_name: string; 56 | /** Type of record affected (maps to resource_type in query) */ 57 | record_type: string | null; 58 | /** Name of the company associated with the record */ 59 | company_name: string | null; 60 | /** URL to the company in Hudu */ 61 | record_company_url: string | null; 62 | /** URL to the user's profile in Hudu */ 63 | record_user_url: string; 64 | /** Type of application where the action occurred */ 65 | app_type: string; 66 | /** ID of the record affected (maps to resource_id in query) */ 67 | record_id: number | null; 68 | /** Name of the record affected */ 69 | record_name: string | null; 70 | /** URL to the record in Hudu */ 71 | record_url: string | null; 72 | /** ISO 8601 timestamp of when the activity occurred */ 73 | created_at: string; 74 | /** Human-readable formatted datetime */ 75 | formatted_datetime: string; 76 | /** User's initials */ 77 | user_initials: string; 78 | /** URL to view the activity in Hudu */ 79 | url: string | null; 80 | /** User agent string of the browser/client */ 81 | agent_string: string | null; 82 | /** Device type used */ 83 | device: string; 84 | /** Operating system used */ 85 | os: string; 86 | } 87 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/api_info/api_info.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleSystemInfoOperation } from '../../utils/operations'; 3 | import type { ApiInfoOperation } from './api_info.types'; 4 | 5 | export async function handleApiInfoOperation( 6 | this: IExecuteFunctions, 7 | operation: ApiInfoOperation, 8 | ): Promise { 9 | let responseData: IDataObject | IDataObject[]; 10 | 11 | switch (operation) { 12 | case 'get': { 13 | responseData = await handleSystemInfoOperation.call( 14 | this, 15 | '/api_info', 16 | ); 17 | break; 18 | } 19 | } 20 | 21 | return responseData; 22 | } 23 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/api_info/api_info.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IApiInfo extends IDataObject { 4 | version: string; 5 | date: string; 6 | } 7 | 8 | export type ApiInfoOperation = 'get'; 9 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/articles/articles.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | import type { FilterMapping } from '../../utils/index'; 3 | 4 | export interface IArticles extends IDataObject { 5 | id: number; // The unique ID of the article 6 | name: string; // The name of the article 7 | content: string; // The HTML content of the article 8 | slug: string; // The url slug of the article 9 | draft: boolean; // A flag that signifies if the article is a draft 10 | url: string; // The url of the article 11 | object_type: string; // The object type is Article 12 | folder_id?: number; // The unique folder ID where the article lives 13 | enable_sharing: boolean; // A flag that signifies if the article is shareable 14 | share_url?: string; // A url for shareable articles 15 | company_id?: number; // The unique company ID for non-global articles 16 | created_at?: string; // The date and time when the article was created 17 | updated_at?: string; // The date and time when the article was last updated 18 | public_photos?: string[]; // A list of public photos 19 | } 20 | 21 | export interface IArticlesResponse extends IDataObject { 22 | article: IArticles; 23 | } 24 | 25 | export type ArticlesOperation = 26 | | 'archive' 27 | | 'create' 28 | | 'delete' 29 | | 'get' 30 | | 'getAll' 31 | | 'getVersionHistory' 32 | | 'unarchive' 33 | | 'update'; 34 | 35 | export interface IArticlePostProcessFilters extends IDataObject { 36 | folder_id?: number; 37 | } 38 | 39 | // Define how each filter should be applied 40 | export const articleFilterMapping: FilterMapping = { 41 | folder_id: (item: IDataObject, value: unknown) => { 42 | return Number(item.folder_id) === Number(value); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/assetCustomField/assetCustomField.types.ts: -------------------------------------------------------------------------------- 1 | export type AssetCustomFieldOperation = 'get' | 'update'; -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/assetLinkField/assetLinkField.types.ts: -------------------------------------------------------------------------------- 1 | export type AssetLinkFieldOperation = 'get' | 'update'; -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/assetStandardField/assetStandardField.types.ts: -------------------------------------------------------------------------------- 1 | export type AssetStandardFieldOperation = 'get' | 'update'; -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/asset_layout_fields/asset_layout_fields.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IAssetLayoutFieldEntity extends IDataObject { 4 | id: number; 5 | label: string; 6 | show_in_list: boolean; 7 | field_type: string; 8 | required?: boolean; 9 | hint: string; 10 | min?: number; 11 | max?: number; 12 | linkable_id?: number; 13 | expiration: boolean; 14 | options: string; 15 | position: number; 16 | is_destroyed: boolean; 17 | } 18 | 19 | export interface IAssetLayoutFieldResponse extends IDataObject { 20 | asset_layout_field: IAssetLayoutFieldEntity; 21 | } 22 | 23 | export type AssetLayoutFieldOperation = 'get' | 'getAll' | 'update' | 'delete' | 'create' | 'reorder'; -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/asset_layouts/asset_layouts.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { processDateRange } from '../../utils/index'; 3 | import { 4 | handleGetAllOperation, 5 | handleGetOperation, 6 | handleCreateOperation, 7 | handleUpdateOperation, 8 | } from '../../utils/operations'; 9 | import type { AssetLayoutOperation } from './asset_layouts.types'; 10 | import type { DateRangePreset } from '../../utils/dateUtils'; 11 | import { debugLog } from '../../utils/debugConfig'; 12 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 13 | 14 | export async function handleAssetLayoutOperation( 15 | this: IExecuteFunctions, 16 | operation: AssetLayoutOperation, 17 | i: number, 18 | ): Promise { 19 | debugLog(`[OPERATION_${operation.toUpperCase()}] Starting asset layout operation`, { operation, index: i }); 20 | const resourceEndpoint = '/asset_layouts'; 21 | let responseData: IDataObject | IDataObject[] = {}; 22 | 23 | switch (operation) { 24 | case 'getAll': { 25 | debugLog('[OPERATION_GET_ALL] Processing get all asset layouts operation'); 26 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 27 | const filters = this.getNodeParameter('filters', i) as IDataObject; 28 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 29 | const qs: IDataObject = { 30 | ...filters, 31 | }; 32 | 33 | debugLog('[RESOURCE_PARAMS] Get all asset layouts parameters', { returnAll, filters, limit }); 34 | 35 | // Process date range if present 36 | if (filters.updated_at) { 37 | const updatedAtFilter = filters.updated_at as IDataObject; 38 | 39 | if (updatedAtFilter.range) { 40 | const rangeObj = updatedAtFilter.range as IDataObject; 41 | 42 | filters.updated_at = processDateRange({ 43 | range: { 44 | mode: rangeObj.mode as 'exact' | 'range' | 'preset', 45 | exact: rangeObj.exact as string, 46 | start: rangeObj.start as string, 47 | end: rangeObj.end as string, 48 | preset: rangeObj.preset as DateRangePreset, 49 | }, 50 | }); 51 | qs.updated_at = filters.updated_at; 52 | debugLog('[UTIL_DATE_PROCESSING] Processed date range', qs.updated_at); 53 | } 54 | } 55 | 56 | debugLog('[API_REQUEST] Getting all asset layouts', { qs, returnAll, limit }); 57 | 58 | responseData = await handleGetAllOperation.call( 59 | this, 60 | resourceEndpoint, 61 | 'asset_layouts', 62 | qs, 63 | returnAll, 64 | limit, 65 | ); 66 | 67 | debugLog('[API_RESPONSE] Get all asset layouts response', responseData); 68 | break; 69 | } 70 | 71 | case 'get': { 72 | debugLog('[OPERATION_GET] Processing get asset layout operation'); 73 | const id = this.getNodeParameter('id', i) as string; 74 | 75 | debugLog('[API_REQUEST] Getting asset layout', { id }); 76 | 77 | responseData = await handleGetOperation.call(this, resourceEndpoint, id); 78 | 79 | debugLog('[API_RESPONSE] Get asset layout response', responseData); 80 | break; 81 | } 82 | 83 | case 'create': { 84 | debugLog('[OPERATION_CREATE] Processing create asset layout operation'); 85 | const name = this.getNodeParameter('name', i) as string; 86 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 87 | const fields = this.getNodeParameter('fields', i) as IDataObject; 88 | 89 | debugLog('[RESOURCE_PARAMS] Create asset layout parameters', { name, additionalFields, fields }); 90 | 91 | const body: IDataObject = { 92 | name, 93 | ...additionalFields, 94 | }; 95 | 96 | if (fields && (fields as IDataObject).field) { 97 | body.fields = (fields as IDataObject).field; 98 | } 99 | 100 | debugLog('[API_REQUEST] Creating asset layout with body', body); 101 | 102 | responseData = await handleCreateOperation.call(this, resourceEndpoint, { asset_layout: body }); 103 | 104 | debugLog('[API_RESPONSE] Create asset layout response', responseData); 105 | break; 106 | } 107 | 108 | case 'update': { 109 | debugLog('[OPERATION_UPDATE] Processing update asset layout operation'); 110 | const id = this.getNodeParameter('id', i) as string; 111 | const name = this.getNodeParameter('name', i) as string; 112 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 113 | const fields = this.getNodeParameter('fields', i) as IDataObject; 114 | 115 | debugLog('[RESOURCE_PARAMS] Update asset layout parameters', { id, name, additionalFields, fields }); 116 | 117 | const body: IDataObject = { 118 | asset_layout: { 119 | name, 120 | ...additionalFields, 121 | }, 122 | }; 123 | 124 | if (fields && (fields as IDataObject).field) { 125 | (body.asset_layout as IDataObject).fields = (fields as IDataObject).field; 126 | } 127 | 128 | debugLog('[API_REQUEST] Updating asset layout with body', body); 129 | 130 | responseData = await handleUpdateOperation.call(this, resourceEndpoint, id, body); 131 | 132 | debugLog('[API_RESPONSE] Update asset layout response', responseData); 133 | break; 134 | } 135 | } 136 | 137 | debugLog(`[OPERATION_${operation.toUpperCase()}] Operation completed`, responseData); 138 | return responseData; 139 | } 140 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/asset_layouts/asset_layouts.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IAssetLayoutField extends IDataObject { 4 | id: number; // The unique identifier for the field 5 | label: string; // The label of the field 6 | show_in_list: boolean; // Whether to show this field in the list view 7 | field_type: string; // The type of the field 8 | required?: boolean; // Whether the field is required (Can be null) 9 | hint: string; // Help text for the field 10 | min?: number; // Minimum value for the field (Can be null) 11 | max?: number; // Maximum value for the field (Can be null) 12 | linkable_id: number; // The ID of the linked item 13 | expiration: boolean; // Whether this field has expiration tracking 14 | options: string; // Field options 15 | position: number; // The position of the field in the layout 16 | is_destroyed: boolean; // Whether this field has been destroyed 17 | } 18 | 19 | export interface IAssetLayout extends IDataObject { 20 | id: number; // The unique identifier for the layout 21 | slug: string; // The URL slug for the layout 22 | name: string; // The name of the layout 23 | icon: string; // The icon for the layout 24 | color: string; // The color for the layout 25 | icon_color: string; // The icon color for the layout 26 | sidebar_folder_id?: number; // The folder ID for the sidebar (Can be null) 27 | active: boolean; // Whether the layout is active 28 | include_passwords: boolean; // Whether to include passwords section 29 | include_photos: boolean; // Whether to include photos section 30 | include_comments: boolean; // Whether to include comments section 31 | include_files: boolean; // Whether to include files section 32 | created_at: string; // When the layout was created (format: date-time) 33 | updated_at: string; // When the layout was last updated (format: date-time) 34 | fields: IAssetLayoutField[]; // The fields defined for this layout 35 | } 36 | 37 | export interface IAssetLayoutResponse extends IDataObject { 38 | asset_layout: IAssetLayout; 39 | } 40 | 41 | export type AssetLayoutOperation = 'create' | 'get' | 'getAll' | 'update'; 42 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/asset_passwords/asset_passwords.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IAssetPassword extends IDataObject { 4 | id: number; // Unique identifier of the asset password 5 | passwordable_type: string; // Type of the related object for the password (e.g., 'Asset', 'Website') 6 | company_id: number; // Identifier of the company to which the password belongs 7 | name: string; // Name of the password 8 | username: string; // Username associated with the password 9 | slug: string; // URL-friendly identifier for the password 10 | description: string; // Description or notes related to the password 11 | password: string; // The actual password string 12 | otp_secret: string; // Secret key for one-time passwords (OTP), if used 13 | url: string; // URL related to the password, if applicable 14 | passwordable_id?: number; // ID of the related object (e.g., 'Website') for the password. Can be null. 15 | password_type?: string; // Type or category of the password. Can be null. 16 | created_at?: string; // Timestamp when the password was created (format: date-time) 17 | updated_at?: string; // Timestamp when the password was last updated (format: date-time) 18 | password_folder_id?: number; // ID of the folder in which the password is stored, if any. Can be null. 19 | password_folder_name?: string; // Name of the folder in which the password is stored, if any. Can be null. 20 | login_url?: string; // URL for the login page associated with the password. Can be null. 21 | in_portal?: boolean; // Whether the password is accessible in the portal 22 | } 23 | 24 | export interface IAssetPasswordResponse extends IDataObject { 25 | asset_password: IAssetPassword; 26 | } 27 | 28 | export type AssetPasswordOperation = 29 | | 'create' 30 | | 'get' 31 | | 'getAll' 32 | | 'update' 33 | | 'delete' 34 | | 'archive' 35 | | 'unarchive'; 36 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/assets/assets.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | interface IAssetField extends IDataObject { 4 | id: number; // The identifier of the field 5 | value: string; // The value stored in the field 6 | label: string; // The label of the field 7 | position: number; // The position of the field in the asset's layout 8 | } 9 | 10 | export interface IAsset extends IDataObject { 11 | id: number; // The unique identifier of the asset 12 | company_id: number; // The identifier of the company to which the asset belongs 13 | asset_layout_id: number; // The identifier of the asset layout associated with the asset 14 | slug: string; // The URL slug used to identify the asset 15 | name: string; // The name of the asset 16 | primary_serial?: string; // The primary serial number of the asset (if available) 17 | primary_mail?: string; // The primary email associated with the asset (if available) 18 | primary_model?: string; // The primary model of the asset (if available) 19 | primary_manufacturer?: string; // The primary manufacturer of the asset (if available) 20 | company_name: string; // The name of the company to which the asset belongs 21 | object_type: string; // The type of object the asset represents 22 | asset_type: string; // The category of the asset 23 | archived: boolean; // Indicates whether the asset is archived or not 24 | url: string; // The URL of the asset page 25 | created_at?: string; // The date and time when the asset was created (format: date-time) 26 | updated_at?: string; // The date and time when the asset was last updated (format: date-time) 27 | fields: IAssetField[]; // A list of fields associated with the asset 28 | cards?: IDataObject[]; // A list of cards associated with the asset (if available) 29 | } 30 | 31 | export interface IAssetResponse extends IDataObject { 32 | assets: IAsset[]; 33 | } 34 | 35 | export type AssetsOperations = 36 | | 'create' 37 | | 'get' 38 | | 'getAll' 39 | | 'update' 40 | | 'delete' 41 | | 'archive' 42 | | 'unarchive' 43 | | 'moveLayout'; 44 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/cards/cards.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleCardLookupOperation, handleCardJumpOperation } from '../../utils/operations/cards'; 3 | import type { CardsOperation } from './cards.types'; 4 | 5 | export async function handleCardOperation( 6 | this: IExecuteFunctions, 7 | operation: CardsOperation, 8 | i: number, 9 | ): Promise { 10 | switch (operation) { 11 | case 'lookup': { 12 | const integrationSlug = this.getNodeParameter('integration_slug', i) as string; 13 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 14 | return handleCardLookupOperation.call(this, integrationSlug, additionalFields); 15 | } 16 | 17 | case 'jump': { 18 | const integrationType = this.getNodeParameter('integration_type', i) as string; 19 | const integrationSlug = this.getNodeParameter('integration_slug', i) as string; 20 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 21 | return handleCardJumpOperation.call(this, integrationType, integrationSlug, additionalFields); 22 | } 23 | 24 | default: 25 | return {}; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/cards/cards.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | import type { INTEGRATION_SLUGS } from '../../utils/constants'; 3 | 4 | export interface ICard extends IDataObject { 5 | integration_slug: string; 6 | integration_id?: string; 7 | integration_identifier?: string; 8 | integration_type?: string; 9 | } 10 | 11 | export interface ICardResponse extends IDataObject { 12 | integrator_cards: ICard[]; 13 | } 14 | 15 | export type CardsOperation = 16 | | 'lookup' // List first as it's the default operation 17 | | 'jump'; 18 | 19 | export interface ICardsJumpParams { 20 | integration_id?: string; 21 | integration_identifier?: string; 22 | integration_slug: (typeof INTEGRATION_SLUGS)[number]; 23 | integration_type: string; 24 | } 25 | 26 | export interface ICardsLookupParams { 27 | integration_id?: string; 28 | integration_identifier?: string; 29 | integration_slug: (typeof INTEGRATION_SLUGS)[number]; 30 | integration_type: string; 31 | } 32 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/companies/companies.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface ICompanyIntegration extends IDataObject { 4 | id: number; // The unique identifier of the integration 5 | integrator_id: number; // The unique identifier of the integrator 6 | integrator_name: string; // The name of the integrator 7 | sync_id: number; // The unique identifier for the synchronization 8 | identifier?: string; // The identifier of the integration (can be null) 9 | name: string; // The name of the integration 10 | potential_company_id?: number; // The unique identifier of the potential company associated with the integration (can be null) 11 | company_id: number; // The unique identifier of the company associated with the integration 12 | company_name: string; // The name of the company associated with the integration 13 | } 14 | 15 | export interface ICompany extends IDataObject { 16 | id: number; // The unique identifier of the company 17 | slug: string; // The URL-friendly identifier of the company 18 | name: string; // The full name of the company 19 | nickname?: string; // The nickname or short name of the company (can be null) 20 | address_line_1: string; // The first line of the company's address 21 | address_line_2?: string; // The second line of the company's address (can be null) 22 | city: string; // The city where the company is located 23 | state: string; // The state or province where the company is located 24 | zip: string; // The zip or postal code of the company's location 25 | country_name?: string; // The name of the country where the company is located (can be null) 26 | phone_number: string; // The company's phone number 27 | company_type?: string; // The type of the company (can be null) 28 | parent_company_id?: number; // The unique identifier of the parent company (can be null) 29 | parent_company_name?: string; // The name of the parent company (can be null) 30 | fax_number: string; // The company's fax number 31 | website: string; // The company's website URL 32 | notes?: string; // Additional notes or information about the company (can be null) 33 | archived: boolean; // Indicates if the company has been archived 34 | object_type: string; // The type of the object, in this case, "Company" 35 | id_number: string; // A custom set identification number 36 | url: string; // The URL path of the company within the application 37 | full_url: string; // The full URL of the company within the application 38 | passwords_url: string; // The URL for the company's passwords within the application 39 | knowledge_base_url: string; // The URL for the company's knowledge base within the application 40 | created_at?: string; // The date and time when the company was created 41 | updated_at?: string; // The date and time when the company was last updated 42 | integrations?: ICompanyIntegration[]; // A list of integrations associated with the company 43 | } 44 | 45 | export interface ICompanyResponse extends IDataObject { 46 | companies: ICompany[]; 47 | } 48 | 49 | export type CompaniesOperations = 50 | | 'create' 51 | | 'get' 52 | | 'getAll' 53 | | 'update' 54 | | 'delete' 55 | | 'archive' 56 | | 'unarchive' 57 | | 'jump'; 58 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/expirations/expirations.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleGetAllOperation } from '../../utils/operations'; 3 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 4 | import type { ExpirationsOperations } from './expirations.types'; 5 | 6 | export async function handleExpirationOperation( 7 | this: IExecuteFunctions, 8 | operation: ExpirationsOperations, 9 | i: number, 10 | ): Promise { 11 | const resourceEndpoint = '/expirations'; 12 | let responseData: IDataObject | IDataObject[]; 13 | 14 | switch (operation) { 15 | case 'getAll': { 16 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 17 | const filters = this.getNodeParameter('filters', i) as IDataObject; 18 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 19 | 20 | responseData = await handleGetAllOperation.call( 21 | this, 22 | resourceEndpoint, 23 | 'expirations', 24 | filters, 25 | returnAll, 26 | limit, 27 | ); 28 | break; 29 | } 30 | } 31 | 32 | return responseData; 33 | } 34 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/expirations/expirations.types.ts: -------------------------------------------------------------------------------- 1 | import { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IExpiration extends IDataObject { 4 | // Required fields 5 | id: number; // The unique identifier for the expiration 6 | date: string; // The expiration date (format: date) 7 | expirationable_type: string; // The type of object associated with the expiration (e.g., Website) 8 | expirationable_id: number; // The ID of the object associated with the expiration 9 | account_id: number; // The account ID associated with the expiration 10 | company_id: number; // The company ID associated with the expiration 11 | expiration_type: string; // The type of expiration (e.g., domain) 12 | 13 | // Optional fields 14 | asset_layout_field_id?: number; // The asset layout field ID associated with the expiration (if any) 15 | sync_id?: number; // The sync ID associated with the expiration (if any) 16 | discarded_at?: string; // The timestamp when the expiration was discarded (if any) (format: date-time) 17 | created_at?: string; // The timestamp when the expiration was created (format: date-time) 18 | updated_at?: string; // The timestamp when the expiration was last updated (format: date-time) 19 | asset_field_id?: number; // The asset field ID associated with the expiration (if any) 20 | } 21 | 22 | export interface IExpirationResponse extends IDataObject { 23 | expirations: IExpiration[]; 24 | } 25 | 26 | export type ExpirationsOperations = 'getAll'; 27 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/folders/folders.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { 3 | handleGetAllOperation, 4 | handleGetOperation, 5 | handleCreateOperation, 6 | handleUpdateOperation, 7 | handleDeleteOperation, 8 | } from '../../utils/operations'; 9 | import type { FilterMapping } from '../../utils/types'; 10 | import type { FolderOperation, IFolderPostProcessFilters, IFolder, IFolderPathResponse } from './folders.types'; 11 | import { folderFilterMapping } from './folders.types'; 12 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 13 | 14 | export async function handleFolderOperation( 15 | this: IExecuteFunctions, 16 | operation: FolderOperation, 17 | i: number, 18 | ): Promise { 19 | const resourceEndpoint = '/folders'; 20 | let responseData: IDataObject | IDataObject[]; 21 | 22 | switch (operation) { 23 | case 'getAll': { 24 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 25 | const filters = this.getNodeParameter('filters', i) as IDataObject; 26 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 27 | 28 | // Extract post-processing filters and API filters separately 29 | const postProcessFilters: IFolderPostProcessFilters = {}; 30 | const apiFilters: IDataObject = {}; 31 | 32 | // Copy only API filters 33 | for (const [key, value] of Object.entries(filters)) { 34 | if (key === 'parent_folder_id') { 35 | postProcessFilters.parent_folder_id = value as number; 36 | } else if (key === 'childFolder') { 37 | postProcessFilters.childFolder = value as 'yes' | 'no' | ''; 38 | } else { 39 | apiFilters[key] = value; 40 | } 41 | } 42 | 43 | responseData = await handleGetAllOperation.call( 44 | this, 45 | resourceEndpoint, 46 | 'folders', 47 | apiFilters, 48 | returnAll, 49 | limit, 50 | postProcessFilters as IDataObject, 51 | folderFilterMapping as FilterMapping, 52 | ); 53 | break; 54 | } 55 | 56 | case 'get': { 57 | const folderId = this.getNodeParameter('folderId', i) as string; 58 | responseData = await handleGetOperation.call(this, resourceEndpoint, folderId); 59 | break; 60 | } 61 | 62 | case 'create': { 63 | const name = this.getNodeParameter('name', i) as string; 64 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 65 | 66 | const body: IDataObject = { 67 | name, 68 | ...additionalFields, 69 | }; 70 | 71 | // Ensure proper field names 72 | if (additionalFields.parent_folder_id === '') { 73 | body.parent_folder_id = null; 74 | } 75 | 76 | if (additionalFields.company_id === '') { 77 | body.company_id = null; 78 | } 79 | 80 | responseData = await handleCreateOperation.call(this, resourceEndpoint, { folder: body }); 81 | break; 82 | } 83 | 84 | case 'update': { 85 | const folderId = this.getNodeParameter('folderId', i) as string; 86 | const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; 87 | 88 | // Ensure proper field handling 89 | if (updateFields.parent_folder_id === '') { 90 | updateFields.parent_folder_id = null; 91 | } 92 | 93 | if (updateFields.company_id === '') { 94 | updateFields.company_id = null; 95 | } 96 | 97 | const body: IDataObject = { 98 | folder: updateFields, 99 | }; 100 | 101 | responseData = await handleUpdateOperation.call(this, resourceEndpoint, folderId, body); 102 | break; 103 | } 104 | 105 | case 'getPath': { 106 | const folderId = this.getNodeParameter('folderId', i) as string; 107 | const folders: IFolder[] = []; 108 | let currentFolderId: string | null = folderId; 109 | 110 | // Recursively get all folders in the path 111 | while (currentFolderId) { 112 | const folderData = await handleGetOperation.call(this, resourceEndpoint, currentFolderId) as IDataObject; 113 | const folder = folderData.folder as IFolder; 114 | folders.unshift(folder); // Add to start of array to maintain correct order 115 | currentFolderId = folder.parent_folder_id ? folder.parent_folder_id.toString() : null; 116 | } 117 | 118 | // Build the path string 119 | const path = folders.map(folder => folder.name).join(' / '); 120 | 121 | responseData = { 122 | path, 123 | folders, 124 | } as IFolderPathResponse; 125 | break; 126 | } 127 | 128 | case 'delete': { 129 | const folderId = this.getNodeParameter('folderId', i) as string; 130 | responseData = await handleDeleteOperation.call(this, resourceEndpoint, folderId); 131 | break; 132 | } 133 | } 134 | 135 | return responseData; 136 | } 137 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/folders/folders.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | import type { FilterMapping } from '../../utils'; 3 | 4 | export interface IFolder extends IDataObject { 5 | id: number; 6 | company_id?: number; 7 | icon?: string; 8 | description?: string; 9 | name: string; 10 | parent_folder_id?: number; 11 | created_at?: string; 12 | updated_at?: string; 13 | } 14 | 15 | export interface IFolderResponse extends IDataObject { 16 | folder: IFolder; 17 | } 18 | 19 | export interface IFolderPostProcessFilters { 20 | parent_folder_id?: number; 21 | childFolder?: 'yes' | 'no' | ''; 22 | [key: string]: unknown; 23 | } 24 | 25 | // Define how each filter should be applied 26 | export const folderFilterMapping: FilterMapping = { 27 | parent_folder_id: (item: IDataObject, value: unknown) => item.parent_folder_id === value, 28 | childFolder: (item: IDataObject, value: unknown) => { 29 | if (value === 'yes') return item.parent_folder_id !== null; 30 | if (value === 'no') return item.parent_folder_id === null; 31 | return true; 32 | }, 33 | }; 34 | 35 | export interface IFolderPathResponse extends IDataObject { 36 | path: string; 37 | folders: IFolder[]; 38 | } 39 | 40 | export type FolderOperation = 'create' | 'get' | 'getAll' | 'update' | 'delete' | 'getPath'; 41 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/index.ts: -------------------------------------------------------------------------------- 1 | // Export all resource handlers and types 2 | export * from './activity_logs/activity_logs.handler'; 3 | export * from './activity_logs/activity_logs.types'; 4 | export * from './api_info/api_info.handler'; 5 | export * from './api_info/api_info.types'; 6 | export * from './articles/articles.handler'; 7 | export * from './articles/articles.types'; 8 | export * from './asset_layouts/asset_layouts.handler'; 9 | export * from './asset_layouts/asset_layouts.types'; 10 | export * from './asset_layout_fields/asset_layout_fields.handler'; 11 | export * from './asset_layout_fields/asset_layout_fields.types'; 12 | export * from './asset_passwords/asset_passwords.handler'; 13 | export * from './asset_passwords/asset_passwords.types'; 14 | export * from './assets/assets.handler'; 15 | export * from './assets/assets.types'; 16 | export * from './cards/cards.handler'; 17 | export * from './cards/cards.types'; 18 | export * from './companies/companies.handler'; 19 | export * from './companies/companies.types'; 20 | export * from './expirations/expirations.handler'; 21 | export * from './expirations/expirations.types'; 22 | export * from './folders/folders.handler'; 23 | export * from './folders/folders.types'; 24 | export * from './ip_addresses/ip_addresses.handler'; 25 | export * from './ip_addresses/ip_addresses.types'; 26 | export * from './list_options/list_options.handler'; 27 | export * from './list_options/list_options.types'; 28 | export * from './magic_dash/magic_dash.handler'; 29 | export * from './magic_dash/magic_dash.types'; 30 | export * from './matchers/matchers.handler'; 31 | export * from './matchers/matchers.types'; 32 | export * from './networks/networks.handler'; 33 | export * from './networks/networks.types'; 34 | export * from './password_folders/password_folders.handler'; 35 | export * from './password_folders/password_folders.types'; 36 | export * from './procedures/procedures.handler'; 37 | export * from './procedures/procedures.types'; 38 | export * from './procedure_tasks/procedure_tasks.handler'; 39 | export * from './procedure_tasks/procedure_tasks.types'; 40 | export * from './public_photos/public_photos.handler'; 41 | export * from './public_photos/public_photos.types'; 42 | export * from './rack_storages/rack_storages.handler'; 43 | export * from './rack_storages/rack_storages.types'; 44 | export * from './rack_storage_items/rack_storage_items.handler'; 45 | export * from './rack_storage_items/rack_storage_items.types'; 46 | export * from './relations/relations.handler'; 47 | export * from './relations/relations.types'; 48 | export * from './uploads/uploads.handler'; 49 | export * from './uploads/uploads.types'; 50 | export * from './users/users.handler'; 51 | export * from './users/users.types'; 52 | export * from './websites/websites.handler'; 53 | export * from './websites/websites.types'; 54 | export * from './lists/lists.handler'; 55 | export * from './lists/lists.types'; 56 | export * from './vlans/vlans.handler'; 57 | export * from './vlans/vlans.types'; 58 | export * from './vlan_zones/vlan_zones.handler'; 59 | export * from './vlan_zones/vlan_zones.types'; 60 | 61 | // Asset Field Custom 62 | export * from './assetCustomField/assetCustomField.types'; 63 | export * from './assetCustomField/assetCustomField.handler'; 64 | 65 | // Asset Link Field 66 | export * from './assetLinkField/assetLinkField.types'; 67 | export * from './assetLinkField/assetLinkField.handler'; 68 | 69 | // Asset Standard Field 70 | export * from './assetStandardField/assetStandardField.types'; 71 | export * from './assetStandardField/assetStandardField.handler'; 72 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/ip_addresses/ip_addresses.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { processDateRange, validateCompanyId } from '../../utils/index'; 3 | import { 4 | handleGetAllOperation, 5 | handleGetOperation, 6 | handleCreateOperation, 7 | handleUpdateOperation, 8 | handleDeleteOperation, 9 | } from '../../utils/operations'; 10 | import type { IpAddressOperations } from './ip_addresses.types'; 11 | import type { DateRangePreset } from '../../utils/dateUtils'; 12 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 13 | import { debugLog } from '../../utils/debugConfig'; 14 | 15 | export async function handleIpAddressesOperation( 16 | this: IExecuteFunctions, 17 | operation: IpAddressOperations, 18 | i: number, 19 | ): Promise { 20 | const resourceEndpoint = '/ip_addresses'; 21 | let responseData: IDataObject | IDataObject[] = {}; 22 | 23 | switch (operation) { 24 | case 'getAll': { 25 | const filters = this.getNodeParameter('filters', i) as IDataObject; 26 | if (filters.company_id) { 27 | filters.company_id = validateCompanyId(filters.company_id, this.getNode(), 'Company ID'); 28 | } 29 | const qs: IDataObject = { 30 | ...filters, 31 | }; 32 | 33 | if (filters.created_at) { 34 | const createdAtFilter = filters.created_at as IDataObject; 35 | if (createdAtFilter.range) { 36 | const rangeObj = createdAtFilter.range as IDataObject; 37 | filters.created_at = processDateRange({ 38 | range: { 39 | mode: rangeObj.mode as 'exact' | 'range' | 'preset', 40 | exact: rangeObj.exact as string, 41 | start: rangeObj.start as string, 42 | end: rangeObj.end as string, 43 | preset: rangeObj.preset as DateRangePreset, 44 | }, 45 | }); 46 | qs.created_at = filters.created_at; 47 | } 48 | } 49 | 50 | if (filters.updated_at) { 51 | const updatedAtFilter = filters.updated_at as IDataObject; 52 | if (updatedAtFilter.range) { 53 | const rangeObj = updatedAtFilter.range as IDataObject; 54 | filters.updated_at = processDateRange({ 55 | range: { 56 | mode: rangeObj.mode as 'exact' | 'range' | 'preset', 57 | exact: rangeObj.exact as string, 58 | start: rangeObj.start as string, 59 | end: rangeObj.end as string, 60 | preset: rangeObj.preset as DateRangePreset, 61 | }, 62 | }); 63 | qs.updated_at = filters.updated_at; 64 | } 65 | } 66 | 67 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 68 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 69 | 70 | responseData = await handleGetAllOperation.call( 71 | this, 72 | resourceEndpoint, 73 | 'ip_addresses', 74 | qs, 75 | returnAll, 76 | limit, 77 | ); 78 | return responseData; 79 | } 80 | 81 | case 'get': { 82 | const id = this.getNodeParameter('id', i) as string; 83 | return await handleGetOperation.call(this, resourceEndpoint, id); 84 | } 85 | 86 | case 'create': { 87 | const address = this.getNodeParameter('address', i) as string; 88 | const status = this.getNodeParameter('status', i) as string; 89 | const companyId = Number.parseInt(this.getNodeParameter('company_id', i) as string, 10); 90 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 91 | 92 | // Map 'comments' to 'notes' for API compatibility 93 | const body: IDataObject = { 94 | address, 95 | status, 96 | company_id: companyId, 97 | ...additionalFields, 98 | }; 99 | 100 | // Map comments to notes if it exists 101 | if (body.comments !== undefined) { 102 | debugLog('[RESOURCE_TRANSFORM] Mapping comments to notes field for IP address', { comments: body.comments }); 103 | body.notes = body.comments; 104 | delete body.comments; 105 | } 106 | 107 | return await handleCreateOperation.call(this, resourceEndpoint, { ip_address: body }); 108 | } 109 | 110 | case 'update': { 111 | const id = this.getNodeParameter('id', i) as string; 112 | const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; 113 | 114 | if (updateFields.company_id) { 115 | updateFields.company_id = Number.parseInt(updateFields.company_id as string, 10); 116 | } 117 | 118 | // Map comments to notes if it exists 119 | if (updateFields.comments !== undefined) { 120 | debugLog('[RESOURCE_TRANSFORM] Mapping comments to notes field for IP address update', { comments: updateFields.comments }); 121 | updateFields.notes = updateFields.comments; 122 | delete updateFields.comments; 123 | } 124 | 125 | return await handleUpdateOperation.call(this, resourceEndpoint, id, { ip_address: updateFields }); 126 | } 127 | 128 | case 'delete': { 129 | const id = this.getNodeParameter('id', i) as string; 130 | return await handleDeleteOperation.call(this, resourceEndpoint, id); 131 | } 132 | 133 | default: 134 | throw new Error(`Operation ${operation} is not supported`); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/ip_addresses/ip_addresses.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export type IpAddressOperations = 'create' | 'delete' | 'get' | 'getAll' | 'update'; 4 | 5 | export interface IpAddress { 6 | id?: number; // The unique identifier for the IP address 7 | address: string; // The IP address 8 | status: 'unassigned' | 'assigned' | 'reserved' | 'deprecated' | 'dhcp' | 'slaac'; // The status of the IP address 9 | fqdn?: string; // The Fully Qualified Domain Name associated with the IP address 10 | description?: string; // A brief description of the IP address 11 | comments?: string; // Additional comments about the IP address (maps to 'notes' in the API) 12 | asset_id?: number; // The identifier of the asset associated with this IP address 13 | network_id?: number; // The identifier of the network to which this IP address belongs 14 | company_id?: number; // The identifier of the company that owns this IP address 15 | skip_dns_validation?: boolean; // If true, the server will not attempt to verify that the FQDN resolves to the address 16 | created_at?: string; // The date and time when the IP address was created 17 | updated_at?: string; // The date and time when the IP address was last updated 18 | } 19 | 20 | export interface IpAddressFilters extends IDataObject { 21 | network_id?: number; 22 | address?: string; 23 | status?: string; 24 | fqdn?: string; 25 | asset_id?: number; 26 | company_id?: number; 27 | created_at?: string; 28 | updated_at?: string; 29 | } 30 | 31 | export interface IpAddressCreateParams { 32 | address: string; 33 | status: string; 34 | fqdn?: string; 35 | description?: string; 36 | comments?: string; 37 | asset_id?: number; 38 | network_id?: number; 39 | company_id?: number; 40 | skip_dns_validation?: boolean; 41 | } 42 | 43 | export interface IpAddressUpdateParams extends IpAddressCreateParams { 44 | id: number; 45 | } 46 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/list_options/list_options.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | import type { IListItem } from '../lists/lists.types'; 3 | 4 | export interface IListOptionsResponse extends IDataObject { 5 | list_options: IListItem[]; // The list items to display as options 6 | } 7 | 8 | export type ListOptionsOperation = 'get' | 'create' | 'update' | 'delete'; -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/lists/lists.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { 3 | handleCreateOperation, 4 | handleGetOperation, 5 | handleGetAllOperation, 6 | handleUpdateOperation, 7 | handleDeleteOperation, 8 | } from '../../utils/operations'; 9 | import type { ListsOperation } from './lists.types'; 10 | import { DEBUG_CONFIG, debugLog } from '../../utils/debugConfig'; 11 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 12 | 13 | export async function handleListsOperation( 14 | this: IExecuteFunctions, 15 | operation: ListsOperation, 16 | i: number, 17 | ): Promise { 18 | const resourceEndpoint = '/lists'; 19 | let responseData: IDataObject | IDataObject[] = {}; 20 | 21 | if (DEBUG_CONFIG.RESOURCE_PROCESSING) { 22 | debugLog('Lists Handler - Input', { 23 | operation, 24 | index: i, 25 | }); 26 | } 27 | 28 | switch (operation) { 29 | case 'create': { 30 | // Name is required 31 | const name = this.getNodeParameter('name', i) as string; 32 | if (!name || name.trim() === '') { 33 | throw new Error('List name cannot be blank'); 34 | } 35 | 36 | const body: IDataObject = { 37 | name, 38 | }; 39 | 40 | responseData = await handleCreateOperation.call( 41 | this, 42 | resourceEndpoint, 43 | { list: body }, 44 | ); 45 | break; 46 | } 47 | case 'get': { 48 | const listId = this.getNodeParameter('id', i) as string; 49 | responseData = await handleGetOperation.call( 50 | this, 51 | resourceEndpoint, 52 | listId, 53 | ); 54 | break; 55 | } 56 | case 'getAll': { 57 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 58 | const filters = this.getNodeParameter('filters', i, {}) as IDataObject; 59 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 60 | const qs: IDataObject = { ...filters }; 61 | responseData = await handleGetAllOperation.call( 62 | this, 63 | resourceEndpoint, 64 | 'lists', 65 | qs, 66 | returnAll, 67 | limit, 68 | ); 69 | break; 70 | } 71 | case 'update': { 72 | const listId = this.getNodeParameter('id', i) as string; 73 | const name = this.getNodeParameter('updateFields.name', i) as string; 74 | if (!name || name.trim() === '') { 75 | throw new Error('List name cannot be blank'); 76 | } 77 | 78 | const updateData: IDataObject = { 79 | name, 80 | }; 81 | 82 | responseData = await handleUpdateOperation.call( 83 | this, 84 | resourceEndpoint, 85 | listId, 86 | { list: updateData }, 87 | ); 88 | break; 89 | } 90 | case 'delete': { 91 | const listId = this.getNodeParameter('id', i) as string; 92 | responseData = await handleDeleteOperation.call( 93 | this, 94 | resourceEndpoint, 95 | listId, 96 | ); 97 | break; 98 | } 99 | default: 100 | throw new Error(`The operation "${operation}" is not supported!`); 101 | } 102 | 103 | if (DEBUG_CONFIG.RESOURCE_PROCESSING) { 104 | debugLog('Lists Handler - Response', responseData); 105 | } 106 | 107 | return responseData; 108 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/lists/lists.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IListItem extends IDataObject { 4 | id?: number; // The unique ID of the list item 5 | name: string; // The name of the list item 6 | _destroy?: boolean; // Used for marking item for deletion in update 7 | } 8 | 9 | export interface IList extends IDataObject { 10 | id: number; // The unique ID of the list 11 | name: string; // The name of the list 12 | list_items: IListItem[]; // The items in the list 13 | created_at?: string; // The date and time when the list was created 14 | updated_at?: string; // The date and time when the list was last updated 15 | } 16 | 17 | export interface IListResponse extends IDataObject { 18 | list: IList; 19 | } 20 | 21 | export type ListsOperation = 22 | | 'create' 23 | | 'get' 24 | | 'getAll' 25 | | 'update' 26 | | 'delete'; -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/magic_dash/magic_dash.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; 2 | import { handleCreateOperation, handleDeleteOperation, handleUpdateOperation } from '../../utils/operations'; 3 | import { 4 | handleMagicDashGetAllOperation, 5 | handleMagicDashGetByIdOperation, 6 | handleMagicDashDeleteByTitleOperation, 7 | } from '../../utils/operations/magic_dash'; 8 | import type { MagicDashOperation } from './magic_dash.types'; 9 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 10 | 11 | export async function handleMagicDashOperation( 12 | this: IExecuteFunctions, 13 | operation: MagicDashOperation, 14 | i: number, 15 | ): Promise { 16 | const resourceEndpoint = '/magic_dash'; 17 | let responseData: IDataObject | IDataObject[] = {}; 18 | 19 | switch (operation) { 20 | case 'getAll': { 21 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 22 | const filters = this.getNodeParameter('filters', i) as IDataObject; 23 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 24 | 25 | return await handleMagicDashGetAllOperation.call(this, filters, returnAll, limit as 25); 26 | } 27 | 28 | case 'get': { 29 | const id = this.getNodeParameter('id', i) as number; 30 | return await handleMagicDashGetByIdOperation.call(this, id); 31 | } 32 | 33 | case 'create': 34 | case 'update': { 35 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 36 | const message = this.getNodeParameter('message', i) as string; 37 | const companyName = this.getNodeParameter('companyName', i) as string; 38 | const title = this.getNodeParameter('title', i) as string; 39 | const content = this.getNodeParameter('content', i) as string; 40 | 41 | // Build the request body 42 | const magicDashBody: IDataObject = { 43 | message, 44 | company_name: companyName, 45 | title, 46 | }; 47 | 48 | // Add content if it's not empty 49 | if (content) { 50 | magicDashBody.content = content; 51 | } 52 | 53 | // Add any additional fields 54 | for (const key of Object.keys(additionalFields)) { 55 | if (additionalFields[key] !== undefined && additionalFields[key] !== '') { 56 | magicDashBody[key] = additionalFields[key]; 57 | } 58 | } 59 | 60 | const body = { 61 | magic_dash: magicDashBody, 62 | }; 63 | 64 | responseData = operation === 'create' 65 | ? await handleCreateOperation.call(this, resourceEndpoint, body) 66 | : await handleUpdateOperation.call(this, resourceEndpoint, title, body); 67 | break; 68 | } 69 | 70 | case 'delete': { 71 | const title = this.getNodeParameter('title', i) as string; 72 | const companyName = this.getNodeParameter('companyName', i) as string; 73 | responseData = await handleMagicDashDeleteByTitleOperation.call(this, title, companyName); 74 | break; 75 | } 76 | 77 | case 'deleteById': { 78 | const id = this.getNodeParameter('id', i) as number; 79 | responseData = await handleDeleteOperation.call(this, resourceEndpoint, id); 80 | break; 81 | } 82 | } 83 | 84 | return responseData; 85 | } 86 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/magic_dash/magic_dash.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IMagicDash extends IDataObject { 4 | id: number; // The unique identifier for the MagicDash item (required) 5 | title: string; // The title of the MagicDash item (required) 6 | message: string; // The primary content to be displayed on the MagicDash item (required) 7 | company_id: number; // The unique identifier of the associated company (required) 8 | company_name: string; // The name of the associated company (required) 9 | shade?: string; // An optional color for the MagicDash item to represent different contextual states (can be null) 10 | content_link?: string; // A link to an external website associated with the MagicDash item's content (can be null) 11 | content?: string; // HTML content (tables, images, videos, etc.) to be displayed in the MagicDash item (can be null) 12 | icon?: string; // A FontAwesome icon for the header of the MagicDash item (can be null) 13 | image_url?: string; // A URL for an image to be used in the header of the MagicDash item (can be null) 14 | } 15 | 16 | export type IMagicDashResponse = IMagicDash[]; 17 | 18 | export type MagicDashOperation = 'getAll' | 'get' | 'create' | 'update' | 'delete' | 'deleteById'; 19 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/matchers/matchers.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleUpdateOperation, handleDeleteOperation } from '../../utils/operations'; 3 | import { handleMatcherGetAllOperation } from '../../utils/operations/matchers'; 4 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 5 | import type { MatcherOperation } from './matchers.types'; 6 | 7 | export async function handleMatcherOperation( 8 | this: IExecuteFunctions, 9 | operation: MatcherOperation, 10 | i: number, 11 | ): Promise { 12 | const resourceEndpoint = '/matchers'; 13 | let responseData: IDataObject | IDataObject[] = {}; 14 | 15 | switch (operation) { 16 | case 'getAll': { 17 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 18 | const filters = this.getNodeParameter('filters', i) as IDataObject; 19 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 20 | const integrationId = this.getNodeParameter('integrationId', i) as number; 21 | 22 | responseData = await handleMatcherGetAllOperation.call( 23 | this, 24 | integrationId, 25 | filters, 26 | returnAll, 27 | limit, 28 | ); 29 | break; 30 | } 31 | 32 | case 'update': { 33 | const id = this.getNodeParameter('id', i) as number; 34 | const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; 35 | 36 | // Build the request body with only defined fields 37 | const matcherUpdate: IDataObject = {}; 38 | 39 | if (updateFields.company_id !== undefined) { 40 | matcherUpdate.company_id = Number.parseInt(updateFields.company_id as string, 10); 41 | } 42 | if (updateFields.potential_company_id !== undefined) { 43 | matcherUpdate.potential_company_id = Number.parseInt( 44 | updateFields.potential_company_id as string, 45 | 10, 46 | ); 47 | } 48 | if (updateFields.sync_id !== undefined) { 49 | matcherUpdate.sync_id = updateFields.sync_id; 50 | } 51 | if (updateFields.identifier !== undefined) { 52 | matcherUpdate.identifier = updateFields.identifier; 53 | } 54 | 55 | responseData = await handleUpdateOperation.call(this, resourceEndpoint, id, { matcher: matcherUpdate }); 56 | break; 57 | } 58 | 59 | case 'delete': { 60 | const id = this.getNodeParameter('id', i) as number; 61 | responseData = await handleDeleteOperation.call(this, resourceEndpoint, id); 62 | break; 63 | } 64 | } 65 | 66 | return responseData; 67 | } 68 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/matchers/matchers.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IMatcher extends IDataObject { 4 | id: number; // The unique identifier for the matcher (required) 5 | integrator_id: number; // The unique identifier for the integrator (required) 6 | integrator_name: string; // The name of the integrator (required) 7 | sync_id: number; // The unique identifier for the synchronization (required) 8 | name: string; // The name of the matcher (required) 9 | identifier?: string; // The identifier in the integration (can be null) 10 | potential_company_id?: number; // The potential company ID (can be null) 11 | company_id?: number; // The company ID (can be null) 12 | company_name?: string; // The name of the company (can be null) 13 | } 14 | 15 | export interface IMatcherResponse extends IDataObject { 16 | matchers: IMatcher[]; 17 | } 18 | 19 | export type MatcherOperation = 'getAll' | 'update' | 'delete'; 20 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/networks/networks.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { processDateRange, type DateRangePreset, validateCompanyId } from '../../utils'; 3 | import { 4 | handleGetAllOperation, 5 | handleGetOperation, 6 | handleCreateOperation, 7 | handleUpdateOperation, 8 | handleDeleteOperation, 9 | } from '../../utils/operations'; 10 | import type { NetworksOperations } from './networks.types'; 11 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 12 | 13 | export async function handleNetworksOperation( 14 | this: IExecuteFunctions, 15 | operation: NetworksOperations, 16 | i: number, 17 | ): Promise { 18 | const resourceEndpoint = '/networks'; 19 | let responseData: IDataObject | IDataObject[] = {}; 20 | 21 | switch (operation) { 22 | case 'create': { 23 | const body: IDataObject = { 24 | name: this.getNodeParameter('name', i) as string, 25 | address: this.getNodeParameter('address', i) as string, 26 | network_type: this.getNodeParameter('network_type', i) as number, 27 | }; 28 | 29 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 30 | 31 | // Only add non-empty additional fields 32 | for (const [key, value] of Object.entries(additionalFields)) { 33 | if (value !== undefined && value !== null && value !== '') { 34 | // Convert company_id to number if present 35 | if (key === 'company_id') { 36 | body[key] = Number.parseInt(value as string, 10); 37 | } else { 38 | body[key] = value; 39 | } 40 | } 41 | } 42 | 43 | responseData = await handleCreateOperation.call(this, resourceEndpoint, { network: body }); 44 | return responseData; 45 | } 46 | 47 | case 'delete': { 48 | const networkId = this.getNodeParameter('networkId', i) as string; 49 | return await handleDeleteOperation.call(this, resourceEndpoint, networkId); 50 | } 51 | 52 | case 'get': { 53 | const networkId = this.getNodeParameter('networkId', i) as string; 54 | return await handleGetOperation.call(this, resourceEndpoint, networkId); 55 | } 56 | 57 | case 'getAll': { 58 | const filters = this.getNodeParameter('filters', i) as IDataObject; 59 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 60 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 61 | const qs: IDataObject = { 62 | ...filters, 63 | }; 64 | 65 | // Validate company_id if provided 66 | if (filters.company_id !== undefined && filters.company_id !== null && filters.company_id !== '') { 67 | qs.company_id = validateCompanyId(filters.company_id, this.getNode(), 'Company ID'); 68 | } 69 | 70 | // Pass archived filter if present 71 | if (filters.archived !== undefined) { 72 | qs.archived = filters.archived; 73 | } 74 | 75 | if (filters.created_at) { 76 | const createdAtFilter = filters.created_at as IDataObject; 77 | if (createdAtFilter.range) { 78 | const rangeObj = createdAtFilter.range as IDataObject; 79 | filters.created_at = processDateRange({ 80 | range: { 81 | mode: rangeObj.mode as 'exact' | 'range' | 'preset', 82 | exact: rangeObj.exact as string, 83 | start: rangeObj.start as string, 84 | end: rangeObj.end as string, 85 | preset: rangeObj.preset as DateRangePreset, 86 | }, 87 | }); 88 | } 89 | } 90 | 91 | if (filters.updated_at) { 92 | const updatedAtFilter = filters.updated_at as IDataObject; 93 | if (updatedAtFilter.range) { 94 | const rangeObj = updatedAtFilter.range as IDataObject; 95 | filters.updated_at = processDateRange({ 96 | range: { 97 | mode: rangeObj.mode as 'exact' | 'range' | 'preset', 98 | exact: rangeObj.exact as string, 99 | start: rangeObj.start as string, 100 | end: rangeObj.end as string, 101 | preset: rangeObj.preset as DateRangePreset, 102 | }, 103 | }); 104 | } 105 | } 106 | 107 | return await handleGetAllOperation.call( 108 | this, 109 | resourceEndpoint, 110 | 'networks', 111 | qs, 112 | returnAll, 113 | limit, 114 | ); 115 | } 116 | 117 | case 'update': { 118 | const networkId = this.getNodeParameter('networkId', i) as string; 119 | const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; 120 | 121 | // Only include non-empty update fields 122 | const networkBody: IDataObject = {}; 123 | for (const [key, value] of Object.entries(updateFields)) { 124 | if (value !== undefined && value !== null && value !== '') { 125 | // Validate company_id if present 126 | if (key === 'company_id') { 127 | networkBody[key] = validateCompanyId(value, this.getNode(), 'Company ID'); 128 | } else { 129 | networkBody[key] = value; 130 | } 131 | } 132 | } 133 | 134 | const body: IDataObject = { 135 | network: networkBody, 136 | }; 137 | 138 | return await handleUpdateOperation.call(this, resourceEndpoint, networkId, body); 139 | } 140 | } 141 | 142 | return responseData; 143 | } 144 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/networks/networks.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface INetwork extends IDataObject { 4 | id?: number; // The unique identifier for the network 5 | name: string; // The name of the network 6 | address: string; // The network address, typically in CIDR notation 7 | network_type: number; // The type of network, represented as an integer 8 | slug?: string; // A slug representing the network 9 | company_id?: number; // The identifier of the company that owns this network 10 | location_id?: number; // The identifier of the location associated with this network 11 | description?: string; // A brief description of the network 12 | created_at?: string; // The date and time when the network was created 13 | updated_at?: string; // The date and time when the network was last updated 14 | archived?: boolean; // Whether the network is archived 15 | notes?: string; // Additional comments about the network 16 | ancestry?: string; // Ancestry path for hierarchical network structure 17 | settings?: IDataObject; // Settings for the network 18 | sync_identifier?: string; // External identifier for synchronisation purposes 19 | is_discovery?: boolean; // Indicates if the network was discovered automatically 20 | status_list_item_id?: number; // The status list item ID for this network 21 | role_list_item_id?: number; // The role list item ID for this network 22 | vlan_id?: number; // The VLAN ID associated with this network 23 | url?: string; // The URL to access this network in the web interface (read-only) 24 | archived_at?: string; // The date and time when the network was archived. Null if not archived (read-only) 25 | } 26 | 27 | export type NetworksOperations = 'create' | 'delete' | 'get' | 'getAll' | 'update'; 28 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/password_folders/password_folders.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleGetOperation, handleGetAllOperation } from '../../utils/operations'; 3 | import type { PasswordFoldersOperations } from './password_folders.types'; 4 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 5 | 6 | export async function handlePasswordFoldersOperation( 7 | this: IExecuteFunctions, 8 | operation: PasswordFoldersOperations, 9 | i: number, 10 | ): Promise { 11 | const resourceEndpoint = '/password_folders'; 12 | let responseData: IDataObject | IDataObject[] = {}; 13 | 14 | switch (operation) { 15 | case 'get': { 16 | const folderId = this.getNodeParameter('id', i) as string; 17 | responseData = await handleGetOperation.call(this, resourceEndpoint, folderId); 18 | break; 19 | } 20 | 21 | case 'getAll': { 22 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 23 | const filters = this.getNodeParameter('filters', i) as IDataObject; 24 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 25 | 26 | const qs: IDataObject = { 27 | ...filters, 28 | }; 29 | 30 | responseData = await handleGetAllOperation.call( 31 | this, 32 | resourceEndpoint, 33 | 'password_folders', 34 | qs, 35 | returnAll, 36 | limit, 37 | ); 38 | break; 39 | } 40 | } 41 | 42 | return responseData; 43 | } 44 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/password_folders/password_folders.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IPasswordFolder extends IDataObject { 4 | id: number; // The unique identifier of the password folder 5 | company_id: number | null; // The ID of the associated company, if any. Can Be null. 6 | description: string; // A brief description of the password folder 7 | name: string; // The name of the password folder 8 | slug: string; // A slug representing the password folder 9 | created_at: string; // The timestamp of password folder creation 10 | updated_at: string; // The timestamp of the last password folder update 11 | } 12 | 13 | export interface IPasswordFolderResponse extends IDataObject { 14 | password_folders: IPasswordFolder[]; 15 | } 16 | 17 | export type PasswordFoldersOperations = 'get' | 'getAll'; 18 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/procedure_tasks/procedure_tasks.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { 3 | handleGetAllOperation, 4 | handleUpdateOperation, 5 | handleCreateOperation, 6 | handleDeleteOperation, 7 | handleGetOperation, 8 | } from '../../utils/operations'; 9 | import type { ProcedureTasksOperations } from './procedure_tasks.types'; 10 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 11 | 12 | export async function handleProcedureTasksOperation( 13 | this: IExecuteFunctions, 14 | operation: ProcedureTasksOperations, 15 | i: number, 16 | ): Promise { 17 | const resourceEndpoint = '/procedure_tasks'; 18 | 19 | switch (operation) { 20 | case 'create': { 21 | const body: IDataObject = { 22 | name: this.getNodeParameter('name', i) as string, 23 | procedure_id: this.getNodeParameter('procedure_id', i) as number, 24 | }; 25 | 26 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 27 | 28 | // Only add non-empty additional fields 29 | for (const [key, value] of Object.entries(additionalFields)) { 30 | if (value !== undefined && value !== null && value !== '') { 31 | body[key] = value; 32 | } 33 | } 34 | 35 | return await handleCreateOperation.call(this, resourceEndpoint, { procedure_task: body }); 36 | } 37 | 38 | case 'delete': { 39 | const taskId = this.getNodeParameter('taskId', i) as string; 40 | return await handleDeleteOperation.call(this, resourceEndpoint, taskId); 41 | } 42 | 43 | case 'get': { 44 | const taskId = this.getNodeParameter('taskId', i) as string; 45 | return await handleGetOperation.call(this, resourceEndpoint, taskId); 46 | } 47 | 48 | case 'getAll': { 49 | const filters = this.getNodeParameter('filters', i) as IDataObject; 50 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 51 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 52 | 53 | const qs: IDataObject = { 54 | ...filters, 55 | }; 56 | 57 | return await handleGetAllOperation.call( 58 | this, 59 | resourceEndpoint, 60 | 'procedure_tasks', 61 | qs, 62 | returnAll, 63 | limit, 64 | ); 65 | } 66 | 67 | case 'update': { 68 | const taskId = this.getNodeParameter('taskId', i) as string; 69 | const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; 70 | 71 | // Only include non-empty update fields 72 | const body: IDataObject = {}; 73 | for (const [key, value] of Object.entries(updateFields)) { 74 | if (value !== undefined && value !== null && value !== '') { 75 | body[key] = value; 76 | } 77 | } 78 | 79 | return await handleUpdateOperation.call(this, resourceEndpoint, taskId, { procedure_task: body }); 80 | } 81 | } 82 | 83 | // This should never be reached due to TypeScript's exhaustive check 84 | throw new Error(`Unsupported operation ${operation}`); 85 | } 86 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/procedure_tasks/procedure_tasks.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IProcedureTask extends IDataObject { 4 | id?: number; // The unique ID of the procedure task 5 | name: string; // The name of the task 6 | description?: string; // A detailed description of the task 7 | procedure_id: number; // The ID of the procedure this task belongs to 8 | position?: number; // The position of the task in the procedure 9 | user_id?: number; // The ID of the user assigned to the task 10 | due_date?: string; // The due date for the task 11 | priority?: 'unsure' | 'low' | 'normal' | 'high' | 'urgent'; // The priority level of the task 12 | assigned_users?: number[]; // An array of user IDs assigned to the task 13 | completed?: boolean; // Indicates whether the task is completed 14 | completed_at?: string; // The date and time when the task was completed 15 | created_at?: string; // The date and time when the task was created 16 | updated_at?: string; // The date and time when the task was last updated 17 | } 18 | 19 | export type ProcedureTasksOperations = 'create' | 'delete' | 'get' | 'getAll' | 'update'; 20 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/procedures/procedures.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IProcedure extends IDataObject { 4 | id: number; // The unique identifier of the procedure 5 | slug: string; // The URL-friendly unique identifier of the procedure 6 | name: string; // The name of the procedure 7 | description?: string; // A brief description of the procedure (can be null) 8 | total: number; // The total number of tasks in the procedure 9 | completed: number; // The number of completed tasks in the procedure 10 | url: string; // The URL for accessing the procedure 11 | object_type: string; // The type of object the procedure represents 12 | company_id?: number; // The unique identifier of the company this procedure belongs to (can be null) 13 | company_name?: string; // The name of the associated company (can be null) 14 | completion_percentage: string; // The completion percentage of the procedure 15 | created_at: string; // The date and time when the procedure was created 16 | updated_at: string; // The date and time when the procedure was last updated 17 | parent_procedure?: string; // The parent procedure, if any (can be null) 18 | asset?: string; // The associated asset, if any (can be null) 19 | share_url: string; // The URL for sharing the procedure 20 | procedure_tasks_attributes?: IDataObject[]; // A list of attributes for the tasks associated with the procedure 21 | } 22 | 23 | export interface IProcedureResponse extends IDataObject { 24 | procedures: IProcedure[]; 25 | } 26 | 27 | export type ProceduresOperations = 28 | | 'create' 29 | | 'delete' 30 | | 'get' 31 | | 'getAll' 32 | | 'update' 33 | | 'archive' 34 | | 'unarchive' 35 | | 'createFromTemplate' 36 | | 'duplicate' 37 | | 'kickoff'; 38 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/public_photos/public_photos.handler.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IExecuteFunctions, 3 | IDataObject, 4 | IHttpRequestMethods, 5 | } from 'n8n-workflow'; 6 | import { huduApiRequest, handleListing } from '../../utils'; 7 | import type { PublicPhotoOperation, IPublicPhoto } from './public_photos.types'; 8 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 9 | import { NodeOperationError } from 'n8n-workflow'; 10 | 11 | export async function handlePublicPhotoOperation( 12 | this: IExecuteFunctions, 13 | operation: PublicPhotoOperation, 14 | i: number, 15 | ): Promise { 16 | let responseData: IDataObject | IDataObject[]; 17 | 18 | switch (operation) { 19 | case 'create': { 20 | // Get the binary property name from the parameter (default: 'data') 21 | const binaryPropertyName = this.getNodeParameter('photo', i) as string; 22 | const recordType = this.getNodeParameter('record_type', i) as string; 23 | const recordId = this.getNodeParameter('record_id', i) as number; 24 | 25 | // Extract binary data from the item 26 | const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); 27 | const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); 28 | 29 | // Prepare form data for multipart upload 30 | const formData = { 31 | photo: { 32 | value: fileBuffer, 33 | options: { 34 | filename: binaryData.fileName || 'photo', 35 | contentType: binaryData.mimeType || 'application/octet-stream', 36 | }, 37 | }, 38 | record_type: recordType, 39 | record_id: recordId, 40 | }; 41 | 42 | // Signal multipart upload to the request utility 43 | (formData as any)._isMultipart = true; 44 | 45 | responseData = await huduApiRequest.call( 46 | this, 47 | 'POST' as IHttpRequestMethods, 48 | '/public_photos', 49 | formData, 50 | ); 51 | break; 52 | } 53 | 54 | case 'getAll': { 55 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 56 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 57 | const filter = this.getNodeParameter('filter', i, {}) as IDataObject; 58 | let recordTypeFilter = ''; 59 | let recordIdFilter: number | null = null; 60 | if (filter && Array.isArray(filter.criteria) && filter.criteria.length > 0) { 61 | const criteria = filter.criteria[0] as IDataObject; 62 | recordTypeFilter = (criteria.record_type_filter as string) || ''; 63 | recordIdFilter = criteria.record_id_filter !== undefined ? (criteria.record_id_filter as number) : null; 64 | } 65 | 66 | let photos = await handleListing.call( 67 | this, 68 | 'GET' as IHttpRequestMethods, 69 | '/public_photos', 70 | 'public_photos', 71 | {}, 72 | {}, 73 | returnAll, 74 | limit, 75 | ) as IPublicPhoto[]; 76 | 77 | if (recordTypeFilter) { 78 | photos = photos.filter(photo => photo.record_type === recordTypeFilter); 79 | } 80 | 81 | if (recordIdFilter !== null) { 82 | photos = photos.filter(photo => photo.record_id === recordIdFilter); 83 | } 84 | 85 | responseData = photos; 86 | break; 87 | } 88 | 89 | case 'get': { 90 | const photoId = this.getNodeParameter('id', i) as number; 91 | if (photoId === undefined || photoId === null) { 92 | throw new NodeOperationError(this.getNode(), 'Photo ID is required for the Get operation.'); 93 | } 94 | 95 | // Efficiently fetch pages of public photos and search for the requested ID 96 | const pageSize = HUDU_API_CONSTANTS.PAGE_SIZE; 97 | let page = 1; 98 | let foundPhoto: IPublicPhoto | undefined; 99 | let hasMore = true; 100 | 101 | while (hasMore && !foundPhoto) { 102 | // Fetch the current page 103 | const queryParams = { page, page_size: pageSize }; 104 | const batch = await huduApiRequest.call( 105 | this, 106 | 'GET' as IHttpRequestMethods, 107 | '/public_photos', 108 | {}, 109 | queryParams, 110 | 'public_photos', 111 | ) as IPublicPhoto[]; 112 | 113 | if (Array.isArray(batch) && batch.length > 0) { 114 | foundPhoto = batch.find(photo => photo.id === photoId); 115 | // If found, break early 116 | if (foundPhoto) { 117 | break; 118 | } 119 | // If batch is less than pageSize, this is the last page 120 | hasMore = batch.length === pageSize; 121 | page++; 122 | } else { 123 | // No more results 124 | hasMore = false; 125 | } 126 | } 127 | 128 | if (!foundPhoto) { 129 | throw new NodeOperationError(this.getNode(), `Public photo with ID "${photoId}" not found.`); 130 | } 131 | responseData = foundPhoto; 132 | break; 133 | } 134 | 135 | case 'update': { 136 | const id = this.getNodeParameter('id', i) as number; 137 | const formData = { 138 | record_type: this.getNodeParameter('record_type', i) as string, 139 | record_id: this.getNodeParameter('record_id', i) as number, 140 | }; 141 | 142 | responseData = await huduApiRequest.call( 143 | this, 144 | 'PUT' as IHttpRequestMethods, 145 | `/public_photos/${id}`, 146 | { public_photo: formData }, 147 | ); 148 | break; 149 | } 150 | } 151 | 152 | return responseData; 153 | } 154 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/public_photos/public_photos.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IPublicPhoto extends IDataObject { 4 | id: number; // The ID of the public photo 5 | url: string; // The URL of the public photo 6 | record_type: string; // The type of record the public photo is associated with (e.g., Article) 7 | record_id: number; // The ID of the record the public photo is associated with 8 | } 9 | 10 | export interface IPublicPhotoResponse extends IDataObject { 11 | public_photo: IPublicPhoto; 12 | } 13 | 14 | export type PublicPhotoOperation = 'getAll' | 'create' | 'update' | 'get'; 15 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/rack_storage_items/rack_storage_items.handler.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IExecuteFunctions, 3 | IDataObject, 4 | } from 'n8n-workflow'; 5 | import { 6 | handleCreateOperation, 7 | handleDeleteOperation, 8 | handleGetOperation, 9 | handleGetAllOperation, 10 | handleUpdateOperation, 11 | } from '../../utils/operations'; 12 | import type { RackStorageItemOperation } from './rack_storage_items.types'; 13 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 14 | 15 | export async function handleRackStorageItemOperation( 16 | this: IExecuteFunctions, 17 | operation: RackStorageItemOperation, 18 | i: number, 19 | ): Promise { 20 | const resourceEndpoint = '/rack_storage_items'; 21 | 22 | switch (operation) { 23 | case 'getAll': { 24 | const filters = this.getNodeParameter('filters', i) as IDataObject; 25 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 26 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 27 | 28 | return await handleGetAllOperation.call( 29 | this, 30 | resourceEndpoint, 31 | 'rack_storage_items', 32 | filters, 33 | returnAll, 34 | limit, 35 | ); 36 | } 37 | 38 | case 'get': { 39 | const id = this.getNodeParameter('id', i) as number; 40 | return await handleGetOperation.call(this, resourceEndpoint, id); 41 | } 42 | 43 | case 'create': { 44 | const body = { 45 | rack_storage_item: { 46 | rack_storage_role_id: this.getNodeParameter('rack_storage_role_id', i) as number, 47 | asset_id: this.getNodeParameter('asset_id', i) as number, 48 | start_unit: this.getNodeParameter('start_unit', i) as number, 49 | end_unit: this.getNodeParameter('end_unit', i) as number, 50 | status: this.getNodeParameter('status', i) as number, 51 | side: this.getNodeParameter('side', i) as number, 52 | ...(this.getNodeParameter('additionalFields', i) as IDataObject), 53 | }, 54 | }; 55 | return await handleCreateOperation.call(this, resourceEndpoint, body); 56 | } 57 | 58 | case 'update': { 59 | const id = this.getNodeParameter('id', i) as number; 60 | const body = { 61 | rack_storage_item: { 62 | ...(this.getNodeParameter('updateFields', i) as IDataObject), 63 | }, 64 | }; 65 | return await handleUpdateOperation.call(this, resourceEndpoint, id, body); 66 | } 67 | 68 | case 'delete': { 69 | const id = this.getNodeParameter('id', i) as number; 70 | return await handleDeleteOperation.call(this, resourceEndpoint, id); 71 | } 72 | } 73 | 74 | // This should never be reached due to TypeScript's exhaustive check 75 | throw new Error(`Unsupported operation ${operation}`); 76 | } 77 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/rack_storage_items/rack_storage_items.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IRackStorageItem extends IDataObject { 4 | id?: number; // The unique ID of the rack storage item 5 | rack_storage_role_id: number; // The unique ID of the rack storage role 6 | asset_id: number; // The unique ID of the asset 7 | start_unit: number; // The start unit of the rack storage item 8 | end_unit: number; // The end unit of the rack storage item 9 | status: number; // The status of the rack storage item 10 | side: number; // The side of the rack storage item 11 | max_wattage?: number; // The maximum wattage of the rack storage item 12 | power_draw?: number; // The power draw of the rack storage item 13 | rack_storage_role_name?: string; // The name of the rack storage role 14 | reserved_message?: string; // The reserved message for the rack storage item 15 | rack_storage_role_description?: string; // The description of the rack storage role 16 | rack_storage_role_hex_color?: string; // The hex color of the rack storage role 17 | asset_name?: string; // The name of the asset 18 | asset_url?: string; // The URL of the asset 19 | url?: string; // The URL of the rack storage item 20 | company_id?: number; // The unique ID of the company 21 | } 22 | 23 | export interface IRackStorageItemResponse extends IDataObject { 24 | rack_storage_item: IRackStorageItem; 25 | } 26 | 27 | export type RackStorageItemOperation = 'getAll' | 'get' | 'create' | 'update' | 'delete'; 28 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/rack_storages/rack_storages.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { processDateRange, validateCompanyId } from '../../utils/index'; 3 | import { 4 | handleCreateOperation, 5 | handleDeleteOperation, 6 | handleGetOperation, 7 | handleGetAllOperation, 8 | handleUpdateOperation, 9 | } from '../../utils/operations'; 10 | import type { RackStorageOperation } from './rack_storages.types'; 11 | import type { DateRangePreset } from '../../utils/dateUtils'; 12 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 13 | 14 | export async function handleRackStorageOperation( 15 | this: IExecuteFunctions, 16 | operation: RackStorageOperation, 17 | i: number, 18 | ): Promise { 19 | const resourceEndpoint = '/rack_storages'; 20 | 21 | switch (operation) { 22 | case 'getAll': { 23 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 24 | const filters = this.getNodeParameter('filters', i) as IDataObject; 25 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 26 | 27 | if (filters.company_id) { 28 | filters.company_id = validateCompanyId(filters.company_id, this.getNode(), 'Company ID'); 29 | } 30 | const qs: IDataObject = { 31 | ...filters, 32 | }; 33 | 34 | // Process date range filters 35 | if (filters.created_at) { 36 | const createdAtFilter = filters.created_at as IDataObject; 37 | if (createdAtFilter.range) { 38 | const rangeObj = createdAtFilter.range as IDataObject; 39 | filters.created_at = processDateRange({ 40 | range: { 41 | mode: rangeObj.mode as 'exact' | 'range' | 'preset', 42 | exact: rangeObj.exact as string, 43 | start: rangeObj.start as string, 44 | end: rangeObj.end as string, 45 | preset: rangeObj.preset as DateRangePreset, 46 | }, 47 | }); 48 | qs.created_at = filters.created_at; 49 | } 50 | } 51 | 52 | if (filters.updated_at) { 53 | const updatedAtFilter = filters.updated_at as IDataObject; 54 | if (updatedAtFilter.range) { 55 | const rangeObj = updatedAtFilter.range as IDataObject; 56 | filters.updated_at = processDateRange({ 57 | range: { 58 | mode: rangeObj.mode as 'exact' | 'range' | 'preset', 59 | exact: rangeObj.exact as string, 60 | start: rangeObj.start as string, 61 | end: rangeObj.end as string, 62 | preset: rangeObj.preset as DateRangePreset, 63 | }, 64 | }); 65 | qs.updated_at = filters.updated_at; 66 | } 67 | } 68 | 69 | return await handleGetAllOperation.call( 70 | this, 71 | resourceEndpoint, 72 | '', 73 | qs, 74 | returnAll, 75 | limit, 76 | ); 77 | } 78 | 79 | case 'get': { 80 | const id = this.getNodeParameter('id', i) as number; 81 | return await handleGetOperation.call(this, resourceEndpoint, id); 82 | } 83 | 84 | case 'create': { 85 | const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; 86 | if (additionalFields.company_id) { 87 | additionalFields.company_id = Number.parseInt(additionalFields.company_id as string, 10); 88 | } 89 | 90 | const body = { 91 | rack_storage: { 92 | name: this.getNodeParameter('name', i) as string, 93 | location_id: this.getNodeParameter('locationId', i) as number, 94 | ...additionalFields, 95 | }, 96 | }; 97 | return await handleCreateOperation.call(this, resourceEndpoint, body); 98 | } 99 | 100 | case 'update': { 101 | const id = this.getNodeParameter('id', i) as number; 102 | const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; 103 | 104 | if (updateFields.company_id) { 105 | updateFields.company_id = Number.parseInt(updateFields.company_id as string, 10); 106 | } 107 | 108 | const body = { 109 | rack_storage: { 110 | ...updateFields, 111 | }, 112 | }; 113 | return await handleUpdateOperation.call(this, resourceEndpoint, id, body); 114 | } 115 | 116 | case 'delete': { 117 | const id = this.getNodeParameter('id', i) as number; 118 | return await handleDeleteOperation.call(this, resourceEndpoint, id); 119 | } 120 | } 121 | 122 | // This should never be reached due to TypeScript's exhaustive check 123 | throw new Error(`Unsupported operation ${operation}`); 124 | } 125 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/rack_storages/rack_storages.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | interface IRackStorageItem { 4 | has_items: boolean; 5 | items: IDataObject[] | null; 6 | number: number; 7 | } 8 | 9 | export interface IRackStorageFilters extends IDataObject { 10 | company_id?: number; // Filter by company id 11 | location_id?: number; // Filter by location id 12 | height?: number; // Filter by rack height 13 | min_width?: number; // Filter by minimum rack width 14 | max_width?: number; // Filter by maximum rack width 15 | created_at?: string; // Filter by creation date range or exact time 16 | updated_at?: string; // Filter by update date range or exact time 17 | } 18 | 19 | export interface IRackStorage extends IDataObject { 20 | id?: number; // The unique ID of the rack storage 21 | location_id: number; // The unique ID of the location of the rack storage 22 | name: string; // The name of the rack storage 23 | description?: string; // The description of the rack storage 24 | descending_units?: boolean; // Whether the units are numbered in descending order 25 | max_wattage?: number | null; // The maximum wattage the rack storage can handle 26 | starting_unit?: number; // The starting unit of the rack storage 27 | height?: number; // The height of the rack storage 28 | width?: number | null; // The width of the rack storage 29 | serial_number?: string; // The serial number of the rack storage 30 | asset_tag?: string; // The asset tag of the rack storage 31 | front_items?: IRackStorageItem[]; // Array of items on the front of the rack 32 | created_at?: string; // The date and time when the rack storage was created 33 | updated_at?: string; // The date and time when the rack storage was last updated 34 | discarded_at?: string; // The date and time when the rack storage was discarded (can be null) 35 | company_id?: number; // The unique ID of the company 36 | } 37 | 38 | export interface IRackStorageResponse extends IDataObject { 39 | rack_storage: IRackStorage; 40 | } 41 | 42 | export type RackStorageOperation = 'getAll' | 'get' | 'create' | 'update' | 'delete'; 43 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/relations/relations.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 3 | import { 4 | handleCreateOperation, 5 | handleDeleteOperation, 6 | handleGetAllOperation, 7 | } from '../../utils/operations'; 8 | import type { RelationOperation } from './relations.types'; 9 | import { relationFilterMapping } from './relations.types'; 10 | 11 | export async function handleRelationsOperation( 12 | this: IExecuteFunctions, 13 | operation: RelationOperation, 14 | i: number, 15 | ): Promise { 16 | const resourceEndpoint = '/relations'; 17 | 18 | switch (operation) { 19 | case 'getAll': { 20 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 21 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 22 | const filters = this.getNodeParameter('filters', i, {}) as IDataObject; 23 | 24 | return await handleGetAllOperation.call( 25 | this, 26 | resourceEndpoint, 27 | 'relations', 28 | {}, 29 | returnAll, 30 | limit, 31 | filters, 32 | relationFilterMapping, 33 | ); 34 | } 35 | 36 | case 'create': { 37 | const body = { 38 | relation: { 39 | toable_id: this.getNodeParameter('toable_id', i) as number, 40 | toable_type: this.getNodeParameter('toable_type', i) as string, 41 | fromable_id: this.getNodeParameter('fromable_id', i) as number, 42 | fromable_type: this.getNodeParameter('fromable_type', i) as string, 43 | description: this.getNodeParameter('description', i) as string, 44 | is_inverse: this.getNodeParameter('is_inverse', i) as boolean, 45 | }, 46 | }; 47 | 48 | return await handleCreateOperation.call(this, resourceEndpoint, body); 49 | } 50 | 51 | case 'delete': { 52 | const id = this.getNodeParameter('id', i) as number; 53 | return await handleDeleteOperation.call(this, resourceEndpoint, id); 54 | } 55 | } 56 | 57 | // This should never be reached due to TypeScript's exhaustive check 58 | throw new Error(`Unsupported operation ${operation}`); 59 | } 60 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/relations/relations.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | import type { FilterMapping } from '../../utils'; 3 | 4 | export interface IRelation extends IDataObject { 5 | id: number; // The unique identifier of the relation 6 | description?: string; // The description of the relation (optional, can be null) 7 | is_inverse: boolean; // Indicates whether the relation is inverse or not 8 | name: string; // The name of the relation 9 | fromable_id: number; // The ID of the origin entity involved in the relation 10 | fromable_type: string; // The type of the origin entity involved in the relation 11 | fromable_url: string; // The URL of the origin entity involved in the relation 12 | toable_id: number; // The ID of the destination entity involved in the relation 13 | toable_type: string; // The type of the destination entity involved in the relation 14 | toable_url: string; // The URL of the destination entity involved in the relation 15 | } 16 | 17 | export interface IRelationResponse extends IDataObject { 18 | relation: IRelation; 19 | } 20 | 21 | export type RelationOperation = 'getAll' | 'create' | 'delete'; 22 | 23 | export type RelationType = 24 | | 'Asset' 25 | | 'Website' 26 | | 'Procedure' 27 | | 'AssetPassword' 28 | | 'Company' 29 | | 'Article'; 30 | 31 | export interface IRelationPostProcessFilters extends IDataObject { 32 | fromable_type?: string; 33 | fromable_id?: number; 34 | toable_type?: string; 35 | toable_id?: number; 36 | is_inverse?: boolean; 37 | } 38 | 39 | // Define how each filter should be applied 40 | export const relationFilterMapping: FilterMapping> = { 41 | fromable_type: (item: IDataObject, value: unknown) => { 42 | return ( 43 | typeof value === 'string' && 44 | typeof item.fromable_type === 'string' && 45 | item.fromable_type.toLowerCase() === value.toLowerCase() 46 | ); 47 | }, 48 | fromable_id: (item: IDataObject, value: unknown) => { 49 | return Number(item.fromable_id) === Number(value); 50 | }, 51 | toable_type: (item: IDataObject, value: unknown) => { 52 | return ( 53 | typeof value === 'string' && 54 | typeof item.toable_type === 'string' && 55 | item.toable_type.toLowerCase() === value.toLowerCase() 56 | ); 57 | }, 58 | toable_id: (item: IDataObject, value: unknown) => { 59 | return Number(item.toable_id) === Number(value); 60 | }, 61 | is_inverse: (item: IDataObject, value: unknown) => { 62 | return Boolean(item.is_inverse) === Boolean(value); 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/uploads/uploads.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject, IHttpRequestMethods } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../../utils'; 3 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 4 | import type { UploadOperation } from './uploads.types'; 5 | 6 | export async function handleUploadOperation( 7 | this: IExecuteFunctions, 8 | operation: UploadOperation, 9 | i: number, 10 | ): Promise { 11 | let responseData: IDataObject | IDataObject[]; 12 | 13 | switch (operation) { 14 | case 'getAll': { 15 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 16 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 17 | 18 | const response = await huduApiRequest.call(this, 'GET' as IHttpRequestMethods, '/uploads'); 19 | responseData = Array.isArray(response) ? response : [response]; 20 | 21 | if (!returnAll && responseData.length > limit) { 22 | responseData = responseData.slice(0, limit); 23 | } 24 | break; 25 | } 26 | 27 | case 'get': { 28 | const id = this.getNodeParameter('id', i) as number; 29 | responseData = await huduApiRequest.call( 30 | this, 31 | 'GET' as IHttpRequestMethods, 32 | `/uploads/${id}`, 33 | ); 34 | break; 35 | } 36 | 37 | case 'delete': { 38 | const id = this.getNodeParameter('id', i) as number; 39 | responseData = await huduApiRequest.call( 40 | this, 41 | 'DELETE' as IHttpRequestMethods, 42 | `/uploads/${id}`, 43 | ); 44 | break; 45 | } 46 | } 47 | 48 | return responseData; 49 | } 50 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/uploads/uploads.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IUpload extends IDataObject { 4 | id: number; // Unique identifier of the upload 5 | url: string; // URL where the file can be accessed 6 | name: string; // Name of the file 7 | ext: string; // File extension 8 | mime: string; // MIME type of the file 9 | size: string; // Size of the file 10 | created_date: string; // Date when the file was uploaded 11 | archived_at?: string; // Date when the file was archived (can be null) 12 | uploadable_id?: number; // ID of the object the file is associated with (can be null) 13 | uploadable_type?: string; // Type of the object the file is associated with (can be null) 14 | } 15 | 16 | export interface IUploadResponse extends IDataObject { 17 | upload: IUpload; 18 | } 19 | 20 | export type UploadOperation = 'getAll' | 'get' | 'delete'; 21 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/users/users.handler.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { HUDU_API_CONSTANTS } from '../../utils/constants'; 3 | import { handleGetOperation, handleGetAllOperation } from '../../utils/operations'; 4 | import type { UserOperation } from './users.types'; 5 | 6 | export async function handleUserOperation( 7 | this: IExecuteFunctions, 8 | operation: UserOperation, 9 | i: number, 10 | ): Promise { 11 | const resourceEndpoint = '/users'; 12 | 13 | switch (operation) { 14 | case 'getAll': { 15 | const returnAll = this.getNodeParameter('returnAll', i) as boolean; 16 | const filters = this.getNodeParameter('filters', i) as IDataObject; 17 | const limit = this.getNodeParameter('limit', i, HUDU_API_CONSTANTS.PAGE_SIZE) as number; 18 | 19 | return await handleGetAllOperation.call( 20 | this, 21 | resourceEndpoint, 22 | 'users', 23 | filters, 24 | returnAll, 25 | limit, 26 | ); 27 | } 28 | 29 | case 'get': { 30 | const id = this.getNodeParameter('id', i) as number; 31 | return await handleGetOperation.call(this, resourceEndpoint, id); 32 | } 33 | } 34 | 35 | // This should never be reached due to TypeScript's exhaustive check 36 | throw new Error(`Unsupported operation ${operation}`); 37 | } 38 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/users/users.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IUser extends IDataObject { 4 | id: number; // The unique identifier of the user 5 | email: string; // The email address of the user 6 | otp_required_for_login: boolean; // Indicates if OTP is required for logging in 7 | security_level: string; // Security level assigned to the user 8 | first_name: string; // The first name of the user 9 | last_name: string; // The last name of the user 10 | phone_number?: string; // The phone number of the user (can be null) 11 | slug: string; // A slug representing the user 12 | time_zone?: string; // The time zone of the user (can be null) 13 | accepted_invite: boolean; // Indicates if the user has accepted an invite 14 | sign_in_count: number; // The number of times the user has signed in 15 | currently_signed_in: boolean; // Indicates if the user is currently signed in 16 | last_sign_in_at?: string; // Timestamp of the last sign-in (can be null) 17 | last_sign_in_ip?: string; // IP address from the last sign-in (can be null) 18 | created_at: string; // The timestamp when the user was created 19 | updated_at?: string; // The timestamp of the last user update (can be null) 20 | archived: boolean; // Indicates if the user is archived (discarded) 21 | portal_member_company_id?: number; // The ID of the associated company for portal members (can be null) 22 | score_30_days?: number; // The user's score over the past 30 days (can be null) 23 | score_all_time?: number; // The user's all-time score (can be null) 24 | score_90_days?: number; // The user's score over the past 90 days (can be null) 25 | } 26 | 27 | export interface IUserResponse extends IDataObject { 28 | user: IUser; 29 | } 30 | 31 | export type UserOperation = 32 | | 'getAll' // List first as it's the default operation 33 | | 'get'; // Get a single user by ID 34 | -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/vlan_zones/vlan_zones.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IVlanZone extends IDataObject { 4 | id?: number; // The unique identifier for the VLAN Zone 5 | name: string; // Human-readable zone name 6 | slug?: string; // URL-friendly identifier 7 | description?: string; // Optional description 8 | vlan_id_ranges: string; // Comma-separated list of numeric ranges (e.g. "100-500,1000-1500") 9 | company_id: number; // The identifier of the company that owns this VLAN Zone 10 | archived?: boolean; // Whether the VLAN Zone is archived (for create/update) 11 | archived_at?: string; // The date and time when the VLAN Zone was archived. Null if not archived. 12 | created_at?: string; // The date and time when the VLAN Zone was created. 13 | updated_at?: string; // The date and time when the VLAN Zone was last updated. 14 | vlans_count?: number; // Number of VLANs currently assigned to this zone 15 | url?: string; // Link to zone in the web UI 16 | } 17 | 18 | export interface IVlanZoneResponse extends IDataObject { 19 | vlan_zone: IVlanZone; 20 | } 21 | 22 | export type VlanZoneOperation = 'getAll' | 'get' | 'create' | 'update' | 'delete'; -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/vlans/vlans.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IVlan extends IDataObject { 4 | id?: number; // The unique identifier for the VLAN 5 | name: string; // Human-readable VLAN name 6 | slug?: string; // URL-friendly identifier 7 | vlan_id: number; // Numeric VLAN (1-4094) 8 | description?: string; // Optional description 9 | notes?: string; // Rich-text notes 10 | company_id: number; // The identifier of the company that owns this VLAN 11 | vlan_zone_id?: number; // Zone (nullable) 12 | status_list_item_id?: number; // The status list item ID for this VLAN 13 | role_list_item_id?: number; // The role list item ID for this VLAN 14 | archived_at?: string; // The date and time when the VLAN was archived. Null if not archived. 15 | created_at?: string; // The date and time when the VLAN was created. 16 | updated_at?: string; // The date and time when the VLAN was last updated. 17 | networks_count?: number; // Number of networks currently assigned to this VLAN. 18 | url?: string; // Link to VLAN in the web UI 19 | } 20 | 21 | export interface IVlanResponse extends IDataObject { 22 | vlan: IVlan; 23 | } 24 | 25 | export type VlanOperation = 'getAll' | 'get' | 'create' | 'update' | 'delete'; -------------------------------------------------------------------------------- /src/nodes/Hudu/resources/websites/websites.types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export interface IWebsite extends IDataObject { 4 | id?: number; // The unique identifier of the website 5 | name: string; // The URL of the website 6 | code?: number; // The HTTP response code of the website 7 | message?: string; // A message related to the website's status 8 | slug?: string; // The URL slug for the website 9 | keyword?: string; // A keyword associated with the website (optional) 10 | monitor_type?: number; // The type of monitoring performed on the website 11 | status?: string; // The status of the website (e.g., 'ready', 'processing') 12 | monitoring_status?: string; // The monitoring status of the website (e.g., 'up', 'down') 13 | refreshed_at?: string; // The timestamp when the website was last refreshed 14 | monitored_at?: string; // The timestamp when the website was last monitored 15 | headers?: object; // HTTP headers associated with the website (optional) 16 | paused?: boolean; // Indicates whether the monitoring of the website is paused 17 | sent_notifications?: boolean; // Indicates whether notifications related to the website have been sent 18 | account_id?: number; // The ID of the associated account 19 | asset_field_id?: number; // The ID of the related asset field (optional) 20 | company_id?: number; // The ID of the associated company 21 | discarded_at?: string; // The timestamp when the website was discarded (optional) 22 | disable_ssl?: boolean; // Indicates whether SSL checks are disabled for the website 23 | disable_whois?: boolean; // Indicates whether WHOIS checks are disabled for the website 24 | disable_dns?: boolean; // Indicates whether DNS checks are disabled for the website 25 | notes?: string; // Additional notes related to the website 26 | object_type?: string; // The type of the object, in this case, 'Website' 27 | icon?: string; // The FontAwesome icon related to the website 28 | asset_type?: string; // The type of the asset, in this case, 'Website' 29 | company_name?: string; // The name of the associated company 30 | url?: string; // The URL path of the website within the application 31 | enable_dmarc_tracking?: boolean; // Indicates whether DMARC checks are enabled for the website 32 | enable_dkim_tracking?: boolean; // Indicates whether DKIM checks are enabled for the website 33 | enable_spf_tracking?: boolean; // Indicates whether SPF checks are enabled for the website 34 | } 35 | 36 | export interface IWebsiteResponse extends IDataObject { 37 | website: IWebsite; 38 | } 39 | 40 | export type WebsiteOperation = 'getAll' | 'get' | 'create' | 'update' | 'delete'; 41 | -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/filterUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter utilities for Hudu API responses 3 | * 4 | * Provides functionality for: 5 | * - Post-processing API response data 6 | * - Type-safe filter function mapping 7 | * - Custom filter application to result sets 8 | * - Flexible filter chaining and composition 9 | */ 10 | 11 | import type { IDataObject } from 'n8n-workflow'; 12 | import { DEBUG_CONFIG, debugLog } from './debugConfig'; 13 | 14 | /** 15 | * Type for filter mapping functions 16 | */ 17 | export type FilterFunction = (item: IDataObject, value: T) => boolean; 18 | 19 | /** 20 | * Type for filter mappings object 21 | */ 22 | export type FilterMapping = { 23 | [P in keyof T]: FilterFunction; 24 | }; 25 | 26 | /** 27 | * Apply post-processing filters to results 28 | */ 29 | export function applyPostFilters( 30 | results: IDataObject[], 31 | filters: T, 32 | filterMapping: Record boolean>, 33 | ): IDataObject[] { 34 | if (DEBUG_CONFIG.UTIL_FILTERS) { 35 | debugLog('Filter Processing - Input', { 36 | resultCount: results.length, 37 | filters, 38 | availableFilters: Object.keys(filterMapping), 39 | }); 40 | } 41 | 42 | try { 43 | // Apply each filter in sequence 44 | let filteredResults = [...results]; 45 | 46 | for (const [key, value] of Object.entries(filters)) { 47 | const filterFn = filterMapping[key]; 48 | if (filterFn && value !== undefined && value !== '') { 49 | if (DEBUG_CONFIG.UTIL_FILTERS) { 50 | debugLog('Filter Processing - Applying Filter', { 51 | filter: key, 52 | value, 53 | resultCountBefore: filteredResults.length, 54 | }); 55 | } 56 | 57 | filteredResults = filteredResults.filter(item => filterFn(item, value)); 58 | 59 | if (DEBUG_CONFIG.UTIL_FILTERS) { 60 | debugLog('Filter Processing - Filter Applied', { 61 | filter: key, 62 | resultCountAfter: filteredResults.length, 63 | }); 64 | } 65 | } 66 | } 67 | 68 | if (DEBUG_CONFIG.UTIL_FILTERS) { 69 | debugLog('Filter Processing - Final Results', { 70 | initialCount: results.length, 71 | finalCount: filteredResults.length, 72 | }); 73 | } 74 | 75 | return filteredResults; 76 | } catch (error) { 77 | if (DEBUG_CONFIG.UTIL_FILTERS) { 78 | debugLog('Filter Processing - Error', { 79 | error, 80 | message: error instanceof Error ? error.message : String(error), 81 | level: 'error', 82 | }); 83 | } 84 | return results; 85 | } 86 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | import { IDataObject } from 'n8n-workflow'; 2 | 3 | /** 4 | * String formatting utilities 5 | */ 6 | 7 | /** 8 | * Formats a string to title case and replaces underscores with spaces 9 | * Example: "hello_world_test" -> "Hello World Test" 10 | */ 11 | export function formatTitleCase(str: string): string { 12 | // First split by underscores 13 | return str 14 | .split('_') 15 | // Then split any camelCase words 16 | .map(word => { 17 | return word 18 | .replace(/([A-Z])/g, ' $1') // Add space before capital letters 19 | .trim() // Remove leading space from first word 20 | .split(' ') // Split into array of words 21 | .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) // Capitalize each word 22 | .join(' '); // Join back with spaces 23 | }) 24 | .join(' '); 25 | } 26 | 27 | export function toSnakeCase(str: string): string { 28 | if (!str) return ''; 29 | return str 30 | .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) // Handle camelCase, PascalCase, and spaces 31 | ?.map(x => x.toLowerCase()) 32 | .join('_') || ''; 33 | } 34 | 35 | /** 36 | * Extract field value from an asset's fields array by field label 37 | * @param fields Array of fields from asset response 38 | * @param fieldLabel The label of the field to find 39 | * @returns The field value or null if not found 40 | */ 41 | export function extractFieldValue(fields: IDataObject[], fieldLabel: string): any { 42 | if (!Array.isArray(fields)) return null; 43 | 44 | const field = fields.find(f => f.label === fieldLabel); 45 | return field ? field.value : null; 46 | } 47 | 48 | /** 49 | * Format custom fields for API payload according to Hudu API requirements 50 | * @param fields Object with field name/value pairs 51 | * @returns Array of single-key objects in the format required by Hudu API 52 | */ 53 | export function formatCustomFields(fields: Record): IDataObject[] { 54 | return Object.entries(fields) 55 | .filter(([_, value]) => value !== null && value !== undefined) 56 | .map(([key, value]) => ({ [toSnakeCase(key)]: value })); 57 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Central export point for Hudu integration utilities 3 | * 4 | * Exports: 5 | * - Date utilities (dateUtils.ts) 6 | * - HTTP request utilities (requestUtils.ts) 7 | * - Filter utilities (filterUtils.ts) 8 | * - Constants and configuration (constants.ts) 9 | * - Validation utilities (validation.ts) 10 | * 11 | * Import all utilities from this file using: 12 | * import { functionName } from '../../utils'; 13 | */ 14 | 15 | export * from './dateUtils'; 16 | export * from './requestUtils'; 17 | export * from './filterUtils'; 18 | export * from './constants'; 19 | export * from './validation'; -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/archive.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | import { DEBUG_CONFIG, debugLog } from '../debugConfig'; 4 | 5 | export async function handleArchiveOperation( 6 | this: IExecuteFunctions, 7 | resourceEndpoint: string, 8 | id: string | number, 9 | archive = true, 10 | companyId?: string | number, 11 | ): Promise { 12 | if (DEBUG_CONFIG.OPERATION_ARCHIVE) { 13 | debugLog('Archive Operation - Input', { 14 | endpoint: resourceEndpoint, 15 | id, 16 | companyId, 17 | action: archive ? 'archive' : 'unarchive', 18 | }); 19 | } 20 | 21 | const endpoint = companyId 22 | ? `/companies/${companyId}${resourceEndpoint}/${id}/${archive ? 'archive' : 'unarchive'}` 23 | : `${resourceEndpoint}/${id}/${archive ? 'archive' : 'unarchive'}`; 24 | 25 | const response = await huduApiRequest.call( 26 | this, 27 | 'PUT', 28 | endpoint, 29 | ); 30 | 31 | if (DEBUG_CONFIG.OPERATION_ARCHIVE) { 32 | debugLog('Archive Operation - Response', response); 33 | } 34 | 35 | return response; 36 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/cards.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | 4 | export async function handleCardLookupOperation( 5 | this: IExecuteFunctions, 6 | integrationSlug: string, 7 | additionalParams: IDataObject = {}, 8 | ): Promise { 9 | const queryParams: IDataObject = { 10 | integration_slug: integrationSlug, 11 | ...additionalParams, 12 | }; 13 | 14 | const response = await huduApiRequest.call( 15 | this, 16 | 'GET', 17 | '/cards/lookup', 18 | undefined, 19 | queryParams, 20 | ); 21 | 22 | return (response as { integrator_cards: IDataObject[] }).integrator_cards || []; 23 | } 24 | 25 | export async function handleCardJumpOperation( 26 | this: IExecuteFunctions, 27 | integrationType: string, 28 | integrationSlug: string, 29 | additionalParams: IDataObject = {}, 30 | ): Promise { 31 | const queryParams: IDataObject = { 32 | integration_type: integrationType, 33 | integration_slug: integrationSlug, 34 | ...additionalParams, 35 | }; 36 | 37 | return await huduApiRequest.call( 38 | this, 39 | 'GET', 40 | '/cards/jump', 41 | undefined, 42 | queryParams, 43 | ); 44 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/companies.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | 4 | export async function handleCompanyJumpOperation( 5 | this: IExecuteFunctions, 6 | integrationSlug: string, 7 | additionalParams: IDataObject = {}, 8 | ): Promise { 9 | const queryParams: IDataObject = { 10 | integration_slug: integrationSlug, 11 | }; 12 | 13 | if (additionalParams.integrationId) { 14 | queryParams.integration_id = additionalParams.integrationId; 15 | } 16 | 17 | if (additionalParams.integrationIdentifier) { 18 | queryParams.integration_identifier = additionalParams.integrationIdentifier; 19 | } 20 | 21 | return await huduApiRequest.call( 22 | this, 23 | 'GET', 24 | '/companies/jump', 25 | {}, 26 | queryParams, 27 | ); 28 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/create.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | import { DEBUG_CONFIG, debugLog } from '../debugConfig'; 4 | 5 | export async function handleCreateOperation( 6 | this: IExecuteFunctions, 7 | resourceEndpoint: string, 8 | body: IDataObject, 9 | ): Promise { 10 | if (DEBUG_CONFIG.OPERATION_CREATE) { 11 | debugLog('Create Operation - Input', { 12 | endpoint: resourceEndpoint, 13 | body, 14 | }); 15 | } 16 | 17 | const response = await huduApiRequest.call( 18 | this, 19 | 'POST', 20 | resourceEndpoint, 21 | body, 22 | ); 23 | 24 | if (DEBUG_CONFIG.OPERATION_CREATE) { 25 | debugLog('Create Operation - Response', response); 26 | } 27 | 28 | return response; 29 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/delete.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | import { DEBUG_CONFIG, debugLog } from '../debugConfig'; 4 | 5 | export async function handleDeleteOperation( 6 | this: IExecuteFunctions, 7 | resourceEndpoint: string, 8 | id: string | number, 9 | companyId?: string | number, 10 | ): Promise { 11 | if (DEBUG_CONFIG.OPERATION_DELETE) { 12 | debugLog('Delete Operation - Input', { 13 | endpoint: resourceEndpoint, 14 | id, 15 | companyId, 16 | }); 17 | } 18 | 19 | const endpoint = companyId 20 | ? `/companies/${companyId}${resourceEndpoint}/${id}` 21 | : `${resourceEndpoint}/${id}`; 22 | 23 | const response = await huduApiRequest.call( 24 | this, 25 | 'DELETE', 26 | endpoint, 27 | ); 28 | 29 | if (DEBUG_CONFIG.OPERATION_DELETE) { 30 | debugLog('Delete Operation - Response', response); 31 | } 32 | 33 | return response; 34 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/get.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, ILoadOptionsFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | import { DEBUG_CONFIG, debugLog } from '../debugConfig'; 4 | 5 | export async function handleGetOperation( 6 | this: IExecuteFunctions | ILoadOptionsFunctions, 7 | resourceEndpoint: string, 8 | id: string | number, 9 | ): Promise { 10 | if (DEBUG_CONFIG.OPERATION_GET) { 11 | debugLog('Get Operation - Input', { 12 | endpoint: resourceEndpoint, 13 | id, 14 | }); 15 | } 16 | 17 | const response = await huduApiRequest.call( 18 | this, 19 | 'GET', 20 | `${resourceEndpoint}/${id}`, 21 | ); 22 | 23 | if (DEBUG_CONFIG.OPERATION_GET) { 24 | debugLog('Get Operation - Response', response); 25 | } 26 | 27 | return response; 28 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/getAll.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { handleListing } from '../requestUtils'; 3 | import type { FilterMapping } from '../types'; 4 | import { DEBUG_CONFIG, debugLog } from '../debugConfig'; 5 | 6 | export async function handleGetAllOperation( 7 | this: IExecuteFunctions, 8 | resourceEndpoint: string, 9 | resourceName: string | undefined, 10 | filters: IDataObject = {}, 11 | returnAll = false, 12 | limit = 25, 13 | postProcessFilters?: IDataObject, 14 | filterMapping?: FilterMapping, 15 | ): Promise { 16 | if (DEBUG_CONFIG.OPERATION_GET_ALL) { 17 | debugLog('GetAll Operation - Input', { 18 | endpoint: resourceEndpoint, 19 | resourceName, 20 | filters, 21 | returnAll, 22 | limit, 23 | hasPostProcessFilters: !!postProcessFilters, 24 | hasFilterMapping: !!filterMapping, 25 | }); 26 | } 27 | 28 | const results = await handleListing.call( 29 | this, 30 | 'GET', 31 | resourceEndpoint, 32 | resourceName, 33 | {}, 34 | filters, 35 | returnAll, 36 | limit, 37 | postProcessFilters, 38 | filterMapping, 39 | ); 40 | 41 | if (DEBUG_CONFIG.OPERATION_GET_ALL) { 42 | debugLog('GetAll Operation - Response', { 43 | totalResults: Array.isArray(results) ? results.length : 1, 44 | results, 45 | }); 46 | } 47 | 48 | return Array.isArray(results) ? results : [results]; 49 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/getCompanyIdForAsset.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { NodeOperationError } from 'n8n-workflow'; 3 | import { huduApiRequest } from '../requestUtils'; 4 | import { debugLog } from '../debugConfig'; 5 | 6 | /** 7 | * Fetches an asset by ID and returns its company ID and the asset object. 8 | * Throws a NodeOperationError if the asset is not found or has no company_id. 9 | * 10 | * @param context n8n execution context (this) 11 | * @param assetId The asset ID to look up 12 | * @param itemIndex Optional item index for error context 13 | * @returns { companyId: string | number, assetObject: IDataObject } 14 | */ 15 | export async function getCompanyIdForAsset( 16 | context: IExecuteFunctions, 17 | assetId: string | number, 18 | itemIndex?: number, 19 | ): Promise<{ companyId: string | number, assetObject: IDataObject }> { 20 | debugLog('[RESOURCE_PROCESSING] Looking up company_id for asset', { assetId }); 21 | const assetLookup = await huduApiRequest.call(context, 'GET', '/assets', {}, { id: String(assetId) }); 22 | const assetsArray = (assetLookup as IDataObject).assets as IDataObject[]; 23 | if (!assetsArray?.length) { 24 | throw new NodeOperationError(context.getNode(), `No asset found with ID '${assetId}'`, { itemIndex }); 25 | } 26 | const assetObject = assetsArray[0]; 27 | const companyId = assetObject.company_id; 28 | if (typeof companyId !== 'string' && typeof companyId !== 'number') { 29 | throw new NodeOperationError(context.getNode(), `Asset with ID '${assetId}' has an invalid company_id type.`, { itemIndex }); 30 | } 31 | debugLog('[RESOURCE_PROCESSING] Found company_id for asset', { assetId, companyId }); 32 | return { companyId, assetObject }; 33 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getAll'; 2 | export * from './get'; 3 | export * from './create'; 4 | export * from './update'; 5 | export * from './delete'; 6 | export * from './archive'; 7 | export * from './systemInfo'; -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/magic_dash.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | import { HUDU_API_CONSTANTS } from '../constants'; 4 | 5 | export async function handleMagicDashGetAllOperation( 6 | this: IExecuteFunctions, 7 | filters: IDataObject = {}, 8 | returnAll = false, 9 | limit = HUDU_API_CONSTANTS.PAGE_SIZE, 10 | ): Promise { 11 | let allItems: IDataObject[] = []; 12 | let page = 1; 13 | const pageSize = HUDU_API_CONSTANTS.PAGE_SIZE; 14 | 15 | let hasMorePages = true; 16 | do { 17 | const qs: IDataObject = { 18 | page, 19 | page_size: pageSize, 20 | }; 21 | 22 | // Handle filters 23 | if (filters.company_id) { 24 | qs.company_id = Number.parseInt(filters.company_id as string, 10); 25 | } 26 | if (filters.title) { 27 | qs.title = filters.title; 28 | } 29 | 30 | const response = await huduApiRequest.call( 31 | this, 32 | 'GET', 33 | '/magic_dash', 34 | {}, 35 | qs, 36 | ); 37 | 38 | const items = Array.isArray(response) ? response : []; 39 | allItems.push(...items); 40 | 41 | if (items.length < pageSize || (!returnAll && allItems.length >= limit)) { 42 | hasMorePages = false; 43 | } else { 44 | page++; 45 | } 46 | } while (hasMorePages); 47 | 48 | if (!returnAll) { 49 | allItems = allItems.slice(0, limit); 50 | } 51 | 52 | return allItems; 53 | } 54 | 55 | export async function handleMagicDashGetByIdOperation( 56 | this: IExecuteFunctions, 57 | id: number, 58 | ): Promise { 59 | const response = await huduApiRequest.call(this, 'GET', '/magic_dash'); 60 | 61 | const items = Array.isArray(response) ? response : []; 62 | const item = items.find((item) => item.id === id); 63 | 64 | if (!item) { 65 | throw new Error(`Magic Dash item with ID ${id} not found`); 66 | } 67 | return item; 68 | } 69 | 70 | export async function handleMagicDashDeleteByTitleOperation( 71 | this: IExecuteFunctions, 72 | title: string, 73 | companyName: string, 74 | ): Promise { 75 | const body = { 76 | title, 77 | company_name: companyName, 78 | }; 79 | 80 | return await huduApiRequest.call( 81 | this, 82 | 'DELETE', 83 | '/magic_dash', 84 | body, 85 | ); 86 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/matchers.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | 4 | export async function handleMatcherGetAllOperation( 5 | this: IExecuteFunctions, 6 | integrationId: number, 7 | filters: IDataObject = {}, 8 | returnAll = false, 9 | limit = 25, 10 | ): Promise { 11 | const queryParams: IDataObject = { 12 | integration_id: integrationId, 13 | }; 14 | 15 | // Add optional filters 16 | if (filters.matched !== undefined) { 17 | queryParams.matched = filters.matched; 18 | } 19 | if (filters.sync_id !== undefined) { 20 | queryParams.sync_id = filters.sync_id; 21 | } 22 | if (filters.identifier !== undefined) { 23 | queryParams.identifier = filters.identifier; 24 | } 25 | if (filters.company_id !== undefined) { 26 | queryParams.company_id = Number.parseInt(filters.company_id as string, 10); 27 | } 28 | if (!returnAll) { 29 | queryParams.page_size = limit; 30 | } 31 | 32 | const response = await huduApiRequest.call( 33 | this, 34 | 'GET', 35 | '/matchers', 36 | undefined, 37 | queryParams, 38 | ); 39 | 40 | // Extract the matchers array from the response 41 | let matchers = Array.isArray(response) 42 | ? response 43 | : ((response as IDataObject).matchers as IDataObject[]) || []; 44 | 45 | if (!returnAll && matchers.length > limit) { 46 | matchers = matchers.slice(0, limit); 47 | } 48 | 49 | return matchers; 50 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/procedures.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | 4 | export async function handleProcedureCreateFromTemplateOperation( 5 | this: IExecuteFunctions, 6 | templateId: string, 7 | additionalFields: IDataObject = {}, 8 | ): Promise { 9 | return await huduApiRequest.call( 10 | this, 11 | 'POST', 12 | `/procedures/${templateId}/create_from_template`, 13 | additionalFields, 14 | ); 15 | } 16 | 17 | export async function handleProcedureDuplicateOperation( 18 | this: IExecuteFunctions, 19 | procedureId: string, 20 | companyId: number, 21 | additionalFields: IDataObject = {}, 22 | ): Promise { 23 | const body: IDataObject = { 24 | company_id: companyId, 25 | ...additionalFields, 26 | }; 27 | 28 | return await huduApiRequest.call( 29 | this, 30 | 'POST', 31 | `/procedures/${procedureId}/duplicate`, 32 | body, 33 | ); 34 | } 35 | 36 | export async function handleProcedureKickoffOperation( 37 | this: IExecuteFunctions, 38 | procedureId: string, 39 | additionalFields: IDataObject, 40 | ): Promise { 41 | const qs: IDataObject = {}; 42 | 43 | if (additionalFields.asset_id) { 44 | qs.asset_id = additionalFields.asset_id; 45 | } 46 | 47 | if (additionalFields.name) { 48 | qs.name = additionalFields.name; 49 | } 50 | 51 | return await huduApiRequest.call( 52 | this, 53 | 'POST', 54 | `/procedures/${procedureId}/kickoff`, 55 | {}, 56 | qs, 57 | ); 58 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/systemInfo.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | 4 | export async function handleSystemInfoOperation( 5 | this: IExecuteFunctions, 6 | endpoint: string, 7 | ): Promise { 8 | return await huduApiRequest.call( 9 | this, 10 | 'GET', 11 | endpoint, 12 | ); 13 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/operations/update.ts: -------------------------------------------------------------------------------- 1 | import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; 2 | import { huduApiRequest } from '../requestUtils'; 3 | import { DEBUG_CONFIG, debugLog } from '../debugConfig'; 4 | 5 | export async function handleUpdateOperation( 6 | this: IExecuteFunctions, 7 | resourceEndpoint: string, 8 | id: string | number, 9 | body: IDataObject, 10 | ): Promise { 11 | if (DEBUG_CONFIG.OPERATION_UPDATE) { 12 | debugLog('Update Operation - Input', { 13 | endpoint: resourceEndpoint, 14 | id, 15 | body, 16 | }); 17 | } 18 | 19 | const response = await huduApiRequest.call( 20 | this, 21 | 'PUT', 22 | `${resourceEndpoint}/${id}`, 23 | body, 24 | ); 25 | 26 | if (DEBUG_CONFIG.OPERATION_UPDATE) { 27 | debugLog('Update Operation - Response', response); 28 | } 29 | 30 | return response; 31 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { IDataObject } from 'n8n-workflow'; 2 | 3 | export type FilterFunction = (item: IDataObject, value: T[keyof T]) => boolean; 4 | 5 | export interface FilterMapping { 6 | [key: string]: FilterFunction; 7 | } -------------------------------------------------------------------------------- /src/nodes/Hudu/utils/validation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Validation utilities for Hudu integration 3 | * 4 | * Provides functionality for: 5 | * - Input validation for common fields 6 | * - Type checking and conversion 7 | * - Standardised error messages 8 | */ 9 | 10 | import type { INode } from 'n8n-workflow'; 11 | import { NodeOperationError } from 'n8n-workflow'; 12 | 13 | /** 14 | * Validates and converts a company ID value to a number 15 | * Throws an error if validation fails 16 | */ 17 | export function validateCompanyId(value: unknown, node: INode, fieldName: string = 'Company ID'): number { 18 | // Handle empty values 19 | if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) { 20 | throw new NodeOperationError(node, `${fieldName} cannot be empty`); 21 | } 22 | 23 | // Convert to number and validate 24 | const numValue = Number(value); 25 | if (isNaN(numValue) || !Number.isInteger(numValue) || numValue < 1) { 26 | throw new NodeOperationError( 27 | node, 28 | `Invalid ${fieldName}: "${value}". Expected a positive integer. If you are using expressions, make sure to reference the ID field.` 29 | ); 30 | } 31 | 32 | return numValue; 33 | } 34 | 35 | /** 36 | * Type guard to check if a value is a valid company ID 37 | */ 38 | export function isValidCompanyId(value: unknown): value is number { 39 | if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) { 40 | return false; 41 | } 42 | 43 | const numValue = Number(value); 44 | return !isNaN(numValue) && Number.isInteger(numValue) && numValue > 0; 45 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "lib": ["es2015", "es2019", "es2020", "es2022.error"], 8 | "removeComments": true, 9 | "useUnknownInCatchVariables": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noErrorTruncation": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "sourceMap": true, 18 | "outDir": "./dist", 19 | "rootDir": ".", 20 | "types": ["node"], 21 | "typeRoots": ["node_modules/@types"], 22 | "resolveJsonModule": true, 23 | "baseUrl": ".", 24 | "paths": { 25 | "@/*": ["src/*"] 26 | }, 27 | "preserveSymlinks": true 28 | }, 29 | "include": ["credentials/**/*", "src/**/*", "index.ts", "tests/**/*"], 30 | "exclude": ["node_modules/**/*", "dist/**/*"] 31 | } 32 | --------------------------------------------------------------------------------