diff --git a/packages/core/bin/generate-ui-types b/packages/core/bin/generate-ui-types index ba7b020b6afc4..8f98f2fc0462f 100755 --- a/packages/core/bin/generate-ui-types +++ b/packages/core/bin/generate-ui-types @@ -9,25 +9,68 @@ LoggerProxy.init({ warn: console.warn.bind(console), }); +function findReferencedMethods(obj, refs = {}, latestName = '') { + for (const key in obj) { + if (key === 'name' && 'group' in obj) { + latestName = obj[key]; + } + + if (typeof obj[key] === 'object') { + findReferencedMethods(obj[key], refs, latestName); + } + + if (key === 'loadOptionsMethod') { + refs[latestName] = refs[latestName] + ? [...new Set([...refs[latestName], obj[key]])] + : [obj[key]]; + } + } + + return refs; +} + (async () => { const loader = new PackageDirectoryLoader(packageDir); - await loader.loadAll(); + await loader.loadAll({ withLoadOptionsMethods: true }); const credentialTypes = Object.values(loader.credentialTypes).map((data) => data.type); - const nodeTypes = Object.values(loader.nodeTypes) + const loaderNodeTypes = Object.values(loader.nodeTypes); + + const definedMethods = loaderNodeTypes.reduce((acc, cur) => { + NodeHelpers.getVersionedNodeTypeAll(cur.type).forEach((type) => { + const methods = type.description?.__loadOptionsMethods; + + if (!methods) return; + + const { name } = type.description; + + acc[name] = acc[name] ? acc[name].push(methods) : methods; + }); + + return acc; + }, {}); + + const nodeTypes = loaderNodeTypes .map((data) => { const nodeType = NodeHelpers.getVersionedNodeType(data.type); NodeHelpers.applySpecialNodeParameters(nodeType); return data.type; }) .flatMap((nodeData) => { - const allNodeTypes = NodeHelpers.getVersionedNodeTypeAll(nodeData); - return allNodeTypes.map((element) => element.description); + return NodeHelpers.getVersionedNodeTypeAll(nodeData).map((item) => { + const { __loadOptionsMethods, ...rest } = item.description; + + return rest; + }); }); + const referencedMethods = findReferencedMethods(nodeTypes); + await Promise.all([ writeJSON('types/credentials.json', credentialTypes), writeJSON('types/nodes.json', nodeTypes), + writeJSON('methods/defined.json', definedMethods), + writeJSON('methods/referenced.json', referencedMethods), ]); })(); diff --git a/packages/core/src/DirectoryLoader.ts b/packages/core/src/DirectoryLoader.ts index 296214a029794..0d2d9020c9bbb 100644 --- a/packages/core/src/DirectoryLoader.ts +++ b/packages/core/src/DirectoryLoader.ts @@ -44,6 +44,8 @@ export abstract class DirectoryLoader { types: Types = { nodes: [], credentials: [] }; + withLoadOptionsMethods = false; // only for validation during build + constructor( readonly directory: string, protected readonly excludeNodes: string[] = [], @@ -103,6 +105,7 @@ export abstract class DirectoryLoader { const currentVersionNode = tempNode.nodeVersions[tempNode.currentVersion]; this.addCodex({ node: currentVersionNode, filePath, isCustom }); nodeVersion = tempNode.currentVersion; + if (this.withLoadOptionsMethods) this.addLoadOptionsMethods(currentVersionNode); if (currentVersionNode.hasOwnProperty('executeSingle')) { Logger.warn( @@ -111,6 +114,7 @@ export abstract class DirectoryLoader { ); } } else { + if (this.withLoadOptionsMethods) this.addLoadOptionsMethods(tempNode); // Short renaming to avoid type issues nodeVersion = Array.isArray(tempNode.description.version) @@ -244,6 +248,12 @@ export abstract class DirectoryLoader { } } + private addLoadOptionsMethods(node: INodeType) { + if (node?.methods?.loadOptions) { + node.description.__loadOptionsMethods = Object.keys(node.methods.loadOptions); + } + } + private fixIconPath( obj: INodeTypeDescription | INodeTypeBaseDescription | ICredentialType, filePath: string, @@ -296,7 +306,9 @@ export class PackageDirectoryLoader extends DirectoryLoader { this.packageName = this.packageJson.name; } - override async loadAll() { + override async loadAll(options = { withLoadOptionsMethods: false }) { + this.withLoadOptionsMethods = options.withLoadOptionsMethods; + await this.readPackageJson(); const { n8n } = this.packageJson; diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/ContactDescription.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/ContactDescription.ts index 22b685dce8423..197f21fe1df59 100644 --- a/packages/nodes-base/nodes/FreshworksCrm/descriptions/ContactDescription.ts +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/ContactDescription.ts @@ -508,9 +508,6 @@ export const contactFields: INodeProperties[] = [ name: 'lead_source_id', type: 'options', default: '', - typeOptions: { - loadOptionsMethod: 'getLeadSources', - }, description: 'ID of the source where contact came from. Choose from the list, or specify an ID using an expression.', }, @@ -580,9 +577,6 @@ export const contactFields: INodeProperties[] = [ name: 'subscription_status', type: 'options', default: '', - typeOptions: { - loadOptionsMethod: 'getSubscriptionStatuses', - }, description: 'Status of subscription that the contact is in. Choose from the list, or specify an ID using an expression.', }, @@ -591,9 +585,6 @@ export const contactFields: INodeProperties[] = [ name: 'subscription_types', type: 'options', default: '', - typeOptions: { - loadOptionsMethod: 'getSubscriptionTypes', - }, description: 'Type of subscription that the contact is in. Choose from the list, or specify an ID using an expression.', }, diff --git a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts index 45e90932b3535..4596c28472abe 100644 --- a/packages/nodes-base/nodes/Paddle/PaymentDescription.ts +++ b/packages/nodes-base/nodes/Paddle/PaymentDescription.ts @@ -184,7 +184,7 @@ export const paymentFields: INodeProperties[] = [ name: 'paymentId', type: 'options', typeOptions: { - loadOptionsMethod: 'getpayment', + loadOptionsMethod: 'getPayments', }, default: '', required: true, diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 34e94c24292f0..8276b02ed9f8a 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -21,7 +21,7 @@ "build:translations": "gulp build:translations", "build:metadata": "pnpm n8n-generate-known && pnpm n8n-generate-ui-types", "format": "prettier --write . --ignore-path ../../.prettierignore", - "lint": "eslint --quiet nodes credentials", + "lint": "eslint --quiet nodes credentials; node ./scripts/validate-load-options-methods.js", "lintfix": "eslint nodes credentials --fix", "watch": "tsc-watch -p tsconfig.build.json --onSuccess \"pnpm n8n-generate-ui-types\"", "test": "jest" diff --git a/packages/nodes-base/scripts/validate-load-options-methods.js b/packages/nodes-base/scripts/validate-load-options-methods.js new file mode 100644 index 0000000000000..9fa11a78222d8 --- /dev/null +++ b/packages/nodes-base/scripts/validate-load-options-methods.js @@ -0,0 +1,43 @@ +let referencedMethods; +let definedMethods; + +try { + referencedMethods = require('../dist/methods/referenced.json'); + definedMethods = require('../dist/methods/defined.json'); +} catch (error) { + console.error( + 'Failed to find methods to validate. Please run `npm run n8n-generate-ui-types` first.', + ); + process.exit(1); +} + +const compareMethods = (base, other) => { + const result = []; + + for (const [nodeName, methods] of Object.entries(base)) { + if (nodeName in other) { + const found = methods.filter((item) => !other[nodeName].includes(item)); + + if (found.length > 0) result.push({ [nodeName]: found }); + } + } + + return result; +}; + +const referencedButUndefined = compareMethods(referencedMethods, definedMethods); + +if (referencedButUndefined.length > 0) { + console.error('ERROR: The following load options methods are referenced but undefined.'); + console.error('Please fix or remove the references or define the methods.'); + console.error(referencedButUndefined); + process.exit(1); +} + +const definedButUnused = compareMethods(definedMethods, referencedMethods); + +if (definedButUnused.length > 0) { + console.warn('Warning: The following load options methods are defined but unused.'); + console.warn('Please consider using or removing the methods.'); + console.warn(definedButUnused); +} diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index e34457d8af535..e1c35590f00b7 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1430,6 +1430,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription { }; }; actions?: INodeActionTypeDescription[]; + __loadOptionsMethods?: string[]; // only for validation during build } export interface INodeHookDescription {