2019-06-23 03:35:23 -07:00
import {
CUSTOM_EXTENSION_ENV ,
UserSettings ,
} from 'n8n-core' ;
import {
2021-06-17 22:58:26 -07:00
CodexData ,
2019-06-23 03:35:23 -07:00
ICredentialType ,
2021-05-21 21:41:06 -07:00
ILogger ,
2019-06-23 03:35:23 -07:00
INodeType ,
2019-08-08 11:38:25 -07:00
INodeTypeData ,
2021-05-21 20:51:38 -07:00
LoggerProxy ,
2019-06-23 03:35:23 -07:00
} from 'n8n-workflow' ;
2021-06-23 02:20:07 -07:00
import * as config from '../config' ;
2021-05-21 20:51:38 -07:00
import {
getLogger ,
} from '../src/Logger' ;
2019-06-24 03:47:44 -07:00
import {
access as fsAccess ,
readdir as fsReaddir ,
readFile as fsReadFile ,
stat as fsStat ,
2021-04-30 19:22:15 -07:00
} from 'fs/promises' ;
2019-06-24 03:47:44 -07:00
import * as glob from 'glob-promise' ;
import * as path from 'path' ;
2019-06-23 03:35:23 -07:00
2021-06-17 22:58:26 -07:00
const CUSTOM_NODES_CATEGORY = 'Custom Nodes' ;
2019-06-23 03:35:23 -07:00
class LoadNodesAndCredentialsClass {
2019-08-08 11:38:25 -07:00
nodeTypes : INodeTypeData = { } ;
2019-06-23 03:35:23 -07:00
credentialTypes : {
[ key : string ] : ICredentialType
} = { } ;
excludeNodes : string [ ] | undefined = undefined ;
2020-12-01 01:53:43 -08:00
includeNodes : string [ ] | undefined = undefined ;
2019-06-23 03:35:23 -07:00
nodeModulesPath = '' ;
2021-05-21 21:41:06 -07:00
logger : ILogger ;
2021-05-21 20:51:38 -07:00
2019-08-08 11:38:25 -07:00
async init() {
2021-05-21 20:51:38 -07:00
this . logger = getLogger ( ) ;
LoggerProxy . init ( this . logger ) ;
2019-06-23 03:35:23 -07:00
// Get the path to the node-modules folder to be later able
// to load the credentials and nodes
const checkPaths = [
// In case "n8n" package is in same node_modules folder.
path . join ( __dirname , '..' , '..' , '..' , 'n8n-workflow' ) ,
// In case "n8n" package is the root and the packages are
// in the "node_modules" folder underneath it.
path . join ( __dirname , '..' , '..' , 'node_modules' , 'n8n-workflow' ) ,
] ;
for ( const checkPath of checkPaths ) {
try {
2021-04-30 19:22:15 -07:00
await fsAccess ( checkPath ) ;
2019-06-23 03:35:23 -07:00
// Folder exists, so use it.
this . nodeModulesPath = path . dirname ( checkPath ) ;
break ;
} catch ( error ) {
// Folder does not exist so get next one
continue ;
}
}
if ( this . nodeModulesPath === '' ) {
throw new Error ( 'Could not find "node_modules" folder!' ) ;
}
2019-07-21 10:47:41 -07:00
this . excludeNodes = config . get ( 'nodes.exclude' ) ;
2020-12-01 01:53:43 -08:00
this . includeNodes = config . get ( 'nodes.include' ) ;
2019-06-23 03:35:23 -07:00
// Get all the installed packages which contain n8n nodes
const packages = await this . getN8nNodePackages ( ) ;
for ( const packageName of packages ) {
await this . loadDataFromPackage ( packageName ) ;
}
// Read nodes and credentials from custom directories
const customDirectories = [ ] ;
// Add "custom" folder in user-n8n folder
customDirectories . push ( UserSettings . getUserN8nFolderCustomExtensionPath ( ) ) ;
// Add folders from special environment variable
if ( process . env [ CUSTOM_EXTENSION_ENV ] !== undefined ) {
const customExtensionFolders = process . env [ CUSTOM_EXTENSION_ENV ] ! . split ( ';' ) ;
customDirectories . push . apply ( customDirectories , customExtensionFolders ) ;
}
for ( const directory of customDirectories ) {
await this . loadDataFromDirectory ( 'CUSTOM' , directory ) ;
}
}
/ * *
* Returns all the names of the packages which could
* contain n8n nodes
*
* @returns { Promise < string [ ] > }
* @memberof LoadNodesAndCredentialsClass
* /
async getN8nNodePackages ( ) : Promise < string [ ] > {
2020-06-03 10:40:39 -07:00
const getN8nNodePackagesRecursive = async ( relativePath : string ) : Promise < string [ ] > = > {
const results : string [ ] = [ ] ;
const nodeModulesPath = ` ${ this . nodeModulesPath } / ${ relativePath } ` ;
2021-04-30 19:22:15 -07:00
for ( const file of await fsReaddir ( nodeModulesPath ) ) {
2020-06-03 10:40:39 -07:00
const isN8nNodesPackage = file . indexOf ( 'n8n-nodes-' ) === 0 ;
const isNpmScopedPackage = file . indexOf ( '@' ) === 0 ;
if ( ! isN8nNodesPackage && ! isNpmScopedPackage ) {
continue ;
}
2021-04-30 19:22:15 -07:00
if ( ! ( await fsStat ( nodeModulesPath ) ) . isDirectory ( ) ) {
2020-06-03 10:40:39 -07:00
continue ;
}
if ( isN8nNodesPackage ) { results . push ( ` ${ relativePath } ${ file } ` ) ; }
if ( isNpmScopedPackage ) {
results . push ( . . . await getN8nNodePackagesRecursive ( ` ${ relativePath } ${ file } / ` ) ) ;
}
2019-06-23 03:35:23 -07:00
}
2020-06-03 10:40:39 -07:00
return results ;
2020-06-03 10:58:55 -07:00
} ;
2020-06-03 10:40:39 -07:00
return getN8nNodePackagesRecursive ( '' ) ;
2019-06-23 03:35:23 -07:00
}
/ * *
* Loads credentials from a file
*
* @param { string } credentialName The name of the credentials
* @param { string } filePath The file to read credentials from
* @returns { Promise < void > }
* /
async loadCredentialsFromFile ( credentialName : string , filePath : string ) : Promise < void > {
const tempModule = require ( filePath ) ;
let tempCredential : ICredentialType ;
try {
tempCredential = new tempModule [ credentialName ] ( ) as ICredentialType ;
} catch ( e ) {
if ( e instanceof TypeError ) {
throw new Error ( ` Class with name " ${ credentialName } " could not be found. Please check if the class is named correctly! ` ) ;
} else {
throw e ;
}
}
2020-01-25 23:48:38 -08:00
this . credentialTypes [ tempCredential . name ] = tempCredential ;
2019-06-23 03:35:23 -07:00
}
/ * *
* Loads a node from a file
*
* @param { string } packageName The package name to set for the found nodes
* @param { string } nodeName Tha name of the node
* @param { string } filePath The file to read node from
* @returns { Promise < void > }
* /
async loadNodeFromFile ( packageName : string , nodeName : string , filePath : string ) : Promise < void > {
let tempNode : INodeType ;
let fullNodeName : string ;
const tempModule = require ( filePath ) ;
try {
tempNode = new tempModule [ nodeName ] ( ) as INodeType ;
2021-06-17 22:58:26 -07:00
this . addCodex ( { node : tempNode , filePath , isCustom : packageName === 'CUSTOM' } ) ;
2019-06-23 03:35:23 -07:00
} catch ( error ) {
console . error ( ` Error loading node " ${ nodeName } " from: " ${ filePath } " ` ) ;
throw error ;
}
fullNodeName = packageName + '.' + tempNode . description . name ;
tempNode . description . name = fullNodeName ;
if ( tempNode . description . icon !== undefined &&
tempNode . description . icon . startsWith ( 'file:' ) ) {
// If a file icon gets used add the full path
tempNode . description . icon = 'file:' + path . join ( path . dirname ( filePath ) , tempNode . description . icon . substr ( 5 ) ) ;
}
2021-05-21 20:51:38 -07:00
if ( tempNode . executeSingle ) {
this . logger . warn ( ` "executeSingle" will get deprecated soon. Please update the code of node " ${ packageName } . ${ nodeName } " to use "execute" instead! ` , { filePath } ) ;
}
2020-12-01 01:53:43 -08:00
if ( this . includeNodes !== undefined && ! this . includeNodes . includes ( fullNodeName ) ) {
return ;
}
2019-08-08 11:38:25 -07:00
// Check if the node should be skiped
2019-06-23 03:35:23 -07:00
if ( this . excludeNodes !== undefined && this . excludeNodes . includes ( fullNodeName ) ) {
return ;
}
2019-08-08 11:38:25 -07:00
this . nodeTypes [ fullNodeName ] = {
type : tempNode ,
sourcePath : filePath ,
} ;
2019-06-23 03:35:23 -07:00
}
2021-06-17 22:58:26 -07:00
/ * *
* Retrieves ` categories ` , ` subcategories ` and alias ( if defined )
* from the codex data for the node at the given file path .
*
* @param { string } filePath The file path to a ` *.node.js ` file
* @returns { CodexData }
* /
getCodex ( filePath : string ) : CodexData {
const { categories , subcategories , alias } = require ( ` ${ filePath } on ` ) ; // .js to .json
return {
. . . ( categories && { categories } ) ,
. . . ( subcategories && { subcategories } ) ,
. . . ( alias && { alias } ) ,
} ;
}
/ * *
* Adds a node codex ` categories ` and ` subcategories ` ( if defined )
* to a node description ` codex ` property .
*
* @param { object } obj
* @param obj . node Node to add categories to
* @param obj . filePath Path to the built node
* @param obj . isCustom Whether the node is custom
* @returns { void }
* /
addCodex ( { node , filePath , isCustom } : {
node : INodeType ;
filePath : string ;
isCustom : boolean ;
} ) {
try {
const codex = this . getCodex ( filePath ) ;
if ( isCustom ) {
codex . categories = codex . categories
? codex . categories . concat ( CUSTOM_NODES_CATEGORY )
: [ CUSTOM_NODES_CATEGORY ] ;
}
node . description . codex = codex ;
} catch ( _ ) {
this . logger . debug ( ` No codex available for: ${ filePath . split ( '/' ) . pop ( ) } ` ) ;
if ( isCustom ) {
node . description . codex = {
categories : [ CUSTOM_NODES_CATEGORY ] ,
} ;
}
}
}
2019-06-23 03:35:23 -07:00
/ * *
* Loads nodes and credentials from the given directory
*
* @param { string } setPackageName The package name to set for the found nodes
* @param { string } directory The directory to look in
* @returns { Promise < void > }
* /
async loadDataFromDirectory ( setPackageName : string , directory : string ) : Promise < void > {
2020-05-22 16:09:05 -07:00
const files = await glob ( path . join ( directory , '**/*\.@(node|credentials)\.js' ) ) ;
2019-06-23 03:35:23 -07:00
let fileName : string ;
let type : string ;
const loadPromises = [ ] ;
for ( const filePath of files ) {
[ fileName , type ] = path . parse ( filePath ) . name . split ( '.' ) ;
if ( type === 'node' ) {
loadPromises . push ( this . loadNodeFromFile ( setPackageName , fileName , filePath ) ) ;
} else if ( type === 'credentials' ) {
loadPromises . push ( this . loadCredentialsFromFile ( fileName , filePath ) ) ;
}
}
await Promise . all ( loadPromises ) ;
}
/ * *
* Loads nodes and credentials from the package with the given name
*
* @param { string } packageName The name to read data from
* @returns { Promise < void > }
* /
async loadDataFromPackage ( packageName : string ) : Promise < void > {
// Get the absolute path of the package
const packagePath = path . join ( this . nodeModulesPath , packageName ) ;
// Read the data from the package.json file to see if any n8n data is defiend
2021-04-30 19:22:15 -07:00
const packageFileString = await fsReadFile ( path . join ( packagePath , 'package.json' ) , 'utf8' ) ;
2019-06-23 03:35:23 -07:00
const packageFile = JSON . parse ( packageFileString ) ;
if ( ! packageFile . hasOwnProperty ( 'n8n' ) ) {
return ;
}
let tempPath : string , filePath : string ;
// Read all node types
let fileName : string , type : string ;
if ( packageFile . n8n . hasOwnProperty ( 'nodes' ) && Array . isArray ( packageFile . n8n . nodes ) ) {
for ( filePath of packageFile . n8n . nodes ) {
tempPath = path . join ( packagePath , filePath ) ;
[ fileName , type ] = path . parse ( filePath ) . name . split ( '.' ) ;
await this . loadNodeFromFile ( packageName , fileName , tempPath ) ;
}
}
// Read all credential types
if ( packageFile . n8n . hasOwnProperty ( 'credentials' ) && Array . isArray ( packageFile . n8n . credentials ) ) {
for ( filePath of packageFile . n8n . credentials ) {
tempPath = path . join ( packagePath , filePath ) ;
[ fileName , type ] = path . parse ( filePath ) . name . split ( '.' ) ;
this . loadCredentialsFromFile ( fileName , tempPath ) ;
}
}
}
}
let packagesInformationInstance : LoadNodesAndCredentialsClass | undefined ;
export function LoadNodesAndCredentials ( ) : LoadNodesAndCredentialsClass {
if ( packagesInformationInstance === undefined ) {
packagesInformationInstance = new LoadNodesAndCredentialsClass ( ) ;
}
return packagesInformationInstance ;
}