diff --git a/cypress/constants.ts b/cypress/constants.ts index 8fdb03ef9cfba..6f7e7b978d317 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -37,7 +37,7 @@ export const INSTANCE_MEMBERS = [ export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking ‘Test workflow’'; export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger'; -export const MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received'; +export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received'; export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const CODE_NODE_NAME = 'Code'; export const SET_NODE_NAME = 'Set'; diff --git a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts index 78d4b61449c23..4c733df90dc48 100644 --- a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts +++ b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts @@ -13,7 +13,7 @@ import { AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, AI_MEMORY_POSTGRES_NODE_NAME, AI_TOOL_CALCULATOR_NODE_NAME, - MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME, + CHAT_TRIGGER_NODE_DISPLAY_NAME, MANUAL_CHAT_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_NAME, @@ -148,7 +148,7 @@ function setupTestWorkflow(chatTrigger: boolean = false) { if (!chatTrigger) { // Remove chat trigger WorkflowPage.getters - .canvasNodeByName(MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME) + .canvasNodeByName(CHAT_TRIGGER_NODE_DISPLAY_NAME) .find('[data-test-id="delete-node-button"]') .click({ force: true }); diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index c1409a34f379b..c6d0f4ab4d3e7 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -10,7 +10,9 @@ import { disableNode, getExecuteWorkflowButton, navigateToNewWorkflowPage, + getNodes, openNode, + getConnectionBySourceAndTarget, } from '../composables/workflow'; import { clickCreateNewCredential, @@ -41,6 +43,7 @@ import { AI_TOOL_WIKIPEDIA_NODE_NAME, BASIC_LLM_CHAIN_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME, + CHAT_TRIGGER_NODE_DISPLAY_NAME, } from './../constants'; describe('Langchain Integration', () => { @@ -331,4 +334,27 @@ describe('Langchain Integration', () => { closeManualChatModal(); }); + + it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => { + addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true); + + getConnectionBySourceAndTarget( + CHAT_TRIGGER_NODE_DISPLAY_NAME, + BASIC_LLM_CHAIN_NODE_NAME, + ).should('exist'); + + getConnectionBySourceAndTarget( + AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, + BASIC_LLM_CHAIN_NODE_NAME, + ).should('exist'); + getNodes().should('have.length', 3); + }); + + it('should not auto-add nodes if AI nodes are already present', () => { + addNodeToCanvas(AGENT_NODE_NAME, true); + + addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true); + getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist'); + getNodes().should('have.length', 3); + }); }); diff --git a/packages/@n8n/config/src/configs/cache.ts b/packages/@n8n/config/src/configs/cache.ts new file mode 100644 index 0000000000000..8a24bdc18beff --- /dev/null +++ b/packages/@n8n/config/src/configs/cache.ts @@ -0,0 +1,36 @@ +import { Config, Env, Nested } from '../decorators'; + +@Config +class MemoryConfig { + /** Max size of memory cache in bytes */ + @Env('N8N_CACHE_MEMORY_MAX_SIZE') + maxSize = 3 * 1024 * 1024; // 3 MiB + + /** Time to live (in milliseconds) for data cached in memory. */ + @Env('N8N_CACHE_MEMORY_TTL') + ttl = 3600 * 1000; // 1 hour +} + +@Config +class RedisConfig { + /** Prefix for cache keys in Redis. */ + @Env('N8N_CACHE_REDIS_KEY_PREFIX') + prefix = 'redis'; + + /** Time to live (in milliseconds) for data cached in Redis. 0 for no TTL. */ + @Env('N8N_CACHE_REDIS_TTL') + ttl = 3600 * 1000; // 1 hour +} + +@Config +export class CacheConfig { + /** Backend to use for caching. */ + @Env('N8N_CACHE_BACKEND') + backend: 'memory' | 'redis' | 'auto' = 'auto'; + + @Nested + memory: MemoryConfig; + + @Nested + redis: RedisConfig; +} diff --git a/packages/@n8n/config/src/configs/credentials.ts b/packages/@n8n/config/src/configs/credentials.ts index 9659061c05e20..ee5f78a681ae6 100644 --- a/packages/@n8n/config/src/configs/credentials.ts +++ b/packages/@n8n/config/src/configs/credentials.ts @@ -7,19 +7,19 @@ class CredentialsOverwrite { * Format: { CREDENTIAL_NAME: { PARAMETER: VALUE }} */ @Env('CREDENTIALS_OVERWRITE_DATA') - readonly data: string = '{}'; + data = '{}'; /** Internal API endpoint to fetch overwritten credential types from. */ @Env('CREDENTIALS_OVERWRITE_ENDPOINT') - readonly endpoint: string = ''; + endpoint = ''; } @Config export class CredentialsConfig { /** Default name for credentials */ @Env('CREDENTIALS_DEFAULT_NAME') - readonly defaultName: string = 'My credentials'; + defaultName = 'My credentials'; @Nested - readonly overwrite: CredentialsOverwrite; + overwrite: CredentialsOverwrite; } diff --git a/packages/@n8n/config/src/configs/database.ts b/packages/@n8n/config/src/configs/database.ts index 384ecb1fb065c..06a3f85465929 100644 --- a/packages/@n8n/config/src/configs/database.ts +++ b/packages/@n8n/config/src/configs/database.ts @@ -4,19 +4,19 @@ import { Config, Env, Nested } from '../decorators'; class LoggingConfig { /** Whether database logging is enabled. */ @Env('DB_LOGGING_ENABLED') - readonly enabled: boolean = false; + enabled = false; /** * Database logging level. Requires `DB_LOGGING_MAX_EXECUTION_TIME` to be higher than `0`. */ @Env('DB_LOGGING_OPTIONS') - readonly options: 'query' | 'error' | 'schema' | 'warn' | 'info' | 'log' | 'all' = 'error'; + options: 'query' | 'error' | 'schema' | 'warn' | 'info' | 'log' | 'all' = 'error'; /** * Only queries that exceed this time (ms) will be logged. Set `0` to disable. */ @Env('DB_LOGGING_MAX_EXECUTION_TIME') - readonly maxQueryExecutionTime: number = 0; + maxQueryExecutionTime = 0; } @Config @@ -26,97 +26,97 @@ class PostgresSSLConfig { * If `DB_POSTGRESDB_SSL_CA`, `DB_POSTGRESDB_SSL_CERT`, or `DB_POSTGRESDB_SSL_KEY` are defined, `DB_POSTGRESDB_SSL_ENABLED` defaults to `true`. */ @Env('DB_POSTGRESDB_SSL_ENABLED') - readonly enabled: boolean = false; + enabled = false; /** SSL certificate authority */ @Env('DB_POSTGRESDB_SSL_CA') - readonly ca: string = ''; + ca = ''; /** SSL certificate */ @Env('DB_POSTGRESDB_SSL_CERT') - readonly cert: string = ''; + cert = ''; /** SSL key */ @Env('DB_POSTGRESDB_SSL_KEY') - readonly key: string = ''; + key = ''; /** If unauthorized SSL connections should be rejected */ @Env('DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED') - readonly rejectUnauthorized: boolean = true; + rejectUnauthorized = true; } @Config class PostgresConfig { /** Postgres database name */ @Env('DB_POSTGRESDB_DATABASE') - database: string = 'n8n'; + database = 'n8n'; /** Postgres database host */ @Env('DB_POSTGRESDB_HOST') - readonly host: string = 'localhost'; + host = 'localhost'; /** Postgres database password */ @Env('DB_POSTGRESDB_PASSWORD') - readonly password: string = ''; + password = ''; /** Postgres database port */ @Env('DB_POSTGRESDB_PORT') - readonly port: number = 5432; + port: number = 5432; /** Postgres database user */ @Env('DB_POSTGRESDB_USER') - readonly user: string = 'postgres'; + user = 'postgres'; /** Postgres database schema */ @Env('DB_POSTGRESDB_SCHEMA') - readonly schema: string = 'public'; + schema = 'public'; /** Postgres database pool size */ @Env('DB_POSTGRESDB_POOL_SIZE') - readonly poolSize = 2; + poolSize = 2; @Nested - readonly ssl: PostgresSSLConfig; + ssl: PostgresSSLConfig; } @Config class MysqlConfig { /** @deprecated MySQL database name */ @Env('DB_MYSQLDB_DATABASE') - database: string = 'n8n'; + database = 'n8n'; /** MySQL database host */ @Env('DB_MYSQLDB_HOST') - readonly host: string = 'localhost'; + host = 'localhost'; /** MySQL database password */ @Env('DB_MYSQLDB_PASSWORD') - readonly password: string = ''; + password = ''; /** MySQL database port */ @Env('DB_MYSQLDB_PORT') - readonly port: number = 3306; + port: number = 3306; /** MySQL database user */ @Env('DB_MYSQLDB_USER') - readonly user: string = 'root'; + user = 'root'; } @Config class SqliteConfig { /** SQLite database file name */ @Env('DB_SQLITE_DATABASE') - readonly database: string = 'database.sqlite'; + database = 'database.sqlite'; /** SQLite database pool size. Set to `0` to disable pooling. */ @Env('DB_SQLITE_POOL_SIZE') - readonly poolSize: number = 0; + poolSize: number = 0; /** * Enable SQLite WAL mode. */ @Env('DB_SQLITE_ENABLE_WAL') - readonly enableWAL: boolean = this.poolSize > 1; + enableWAL = this.poolSize > 1; /** * Run `VACUUM` on startup to rebuild the database, reducing file size and optimizing indexes. @@ -124,7 +124,7 @@ class SqliteConfig { * @warning Long-running blocking operation that will increase startup time. */ @Env('DB_SQLITE_VACUUM_ON_STARTUP') - readonly executeVacuumOnStartup: boolean = false; + executeVacuumOnStartup = false; } @Config @@ -135,17 +135,17 @@ export class DatabaseConfig { /** Prefix for table names */ @Env('DB_TABLE_PREFIX') - readonly tablePrefix: string = ''; + tablePrefix = ''; @Nested - readonly logging: LoggingConfig; + logging: LoggingConfig; @Nested - readonly postgresdb: PostgresConfig; + postgresdb: PostgresConfig; @Nested - readonly mysqldb: MysqlConfig; + mysqldb: MysqlConfig; @Nested - readonly sqlite: SqliteConfig; + sqlite: SqliteConfig; } diff --git a/packages/@n8n/config/src/configs/email.ts b/packages/@n8n/config/src/configs/email.ts index 318c35238070f..f0e130c3b48be 100644 --- a/packages/@n8n/config/src/configs/email.ts +++ b/packages/@n8n/config/src/configs/email.ts @@ -4,75 +4,75 @@ import { Config, Env, Nested } from '../decorators'; export class SmtpAuth { /** SMTP login username */ @Env('N8N_SMTP_USER') - readonly user: string = ''; + user = ''; /** SMTP login password */ @Env('N8N_SMTP_PASS') - readonly pass: string = ''; + pass = ''; /** SMTP OAuth Service Client */ @Env('N8N_SMTP_OAUTH_SERVICE_CLIENT') - readonly serviceClient: string = ''; + serviceClient = ''; /** SMTP OAuth Private Key */ @Env('N8N_SMTP_OAUTH_PRIVATE_KEY') - readonly privateKey: string = ''; + privateKey = ''; } @Config export class SmtpConfig { /** SMTP server host */ @Env('N8N_SMTP_HOST') - readonly host: string = ''; + host = ''; /** SMTP server port */ @Env('N8N_SMTP_PORT') - readonly port: number = 465; + port: number = 465; /** Whether to use SSL for SMTP */ @Env('N8N_SMTP_SSL') - readonly secure: boolean = true; + secure: boolean = true; /** Whether to use STARTTLS for SMTP when SSL is disabled */ @Env('N8N_SMTP_STARTTLS') - readonly startTLS: boolean = true; + startTLS: boolean = true; /** How to display sender name */ @Env('N8N_SMTP_SENDER') - readonly sender: string = ''; + sender = ''; @Nested - readonly auth: SmtpAuth; + auth: SmtpAuth; } @Config export class TemplateConfig { /** Overrides default HTML template for inviting new people (use full path) */ @Env('N8N_UM_EMAIL_TEMPLATES_INVITE') - readonly invite: string = ''; + invite = ''; /** Overrides default HTML template for resetting password (use full path) */ @Env('N8N_UM_EMAIL_TEMPLATES_PWRESET') - readonly passwordReset: string = ''; + passwordReset = ''; /** Overrides default HTML template for notifying that a workflow was shared (use full path) */ @Env('N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED') - readonly workflowShared: string = ''; + workflowShared = ''; /** Overrides default HTML template for notifying that credentials were shared (use full path) */ @Env('N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED') - readonly credentialsShared: string = ''; + credentialsShared = ''; } @Config export class EmailConfig { /** How to send emails */ @Env('N8N_EMAIL_MODE') - readonly mode: '' | 'smtp' = 'smtp'; + mode: '' | 'smtp' = 'smtp'; @Nested - readonly smtp: SmtpConfig; + smtp: SmtpConfig; @Nested - readonly template: TemplateConfig; + template: TemplateConfig; } diff --git a/packages/@n8n/config/src/configs/endpoints.ts b/packages/@n8n/config/src/configs/endpoints.ts index 7a04a8249f692..4957c5afa58d6 100644 --- a/packages/@n8n/config/src/configs/endpoints.ts +++ b/packages/@n8n/config/src/configs/endpoints.ts @@ -4,99 +4,99 @@ import { Config, Env, Nested } from '../decorators'; class PrometheusMetricsConfig { /** Whether to enable the `/metrics` endpoint to expose Prometheus metrics. */ @Env('N8N_METRICS') - readonly enable: boolean = false; + enable = false; /** Prefix for Prometheus metric names. */ @Env('N8N_METRICS_PREFIX') - readonly prefix: string = 'n8n_'; + prefix = 'n8n_'; /** Whether to expose system and Node.js metrics. See: https://www.npmjs.com/package/prom-client */ @Env('N8N_METRICS_INCLUDE_DEFAULT_METRICS') - readonly includeDefaultMetrics = true; + includeDefaultMetrics = true; /** Whether to include a label for workflow ID on workflow metrics. */ @Env('N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL') - readonly includeWorkflowIdLabel: boolean = false; + includeWorkflowIdLabel = false; /** Whether to include a label for node type on node metrics. */ @Env('N8N_METRICS_INCLUDE_NODE_TYPE_LABEL') - readonly includeNodeTypeLabel: boolean = false; + includeNodeTypeLabel = false; /** Whether to include a label for credential type on credential metrics. */ @Env('N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL') - readonly includeCredentialTypeLabel: boolean = false; + includeCredentialTypeLabel = false; /** Whether to expose metrics for API endpoints. See: https://www.npmjs.com/package/express-prom-bundle */ @Env('N8N_METRICS_INCLUDE_API_ENDPOINTS') - readonly includeApiEndpoints: boolean = false; + includeApiEndpoints = false; /** Whether to include a label for the path of API endpoint calls. */ @Env('N8N_METRICS_INCLUDE_API_PATH_LABEL') - readonly includeApiPathLabel: boolean = false; + includeApiPathLabel = false; /** Whether to include a label for the HTTP method of API endpoint calls. */ @Env('N8N_METRICS_INCLUDE_API_METHOD_LABEL') - readonly includeApiMethodLabel: boolean = false; + includeApiMethodLabel = false; /** Whether to include a label for the status code of API endpoint calls. */ @Env('N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL') - readonly includeApiStatusCodeLabel: boolean = false; + includeApiStatusCodeLabel = false; /** Whether to include metrics for cache hits and misses. */ @Env('N8N_METRICS_INCLUDE_CACHE_METRICS') - readonly includeCacheMetrics: boolean = false; + includeCacheMetrics = false; /** Whether to include metrics derived from n8n's internal events */ @Env('N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS') - readonly includeMessageEventBusMetrics: boolean = false; + includeMessageEventBusMetrics = false; } @Config export class EndpointsConfig { /** Max payload size in MiB */ @Env('N8N_PAYLOAD_SIZE_MAX') - readonly payloadSizeMax: number = 16; + payloadSizeMax: number = 16; @Nested - readonly metrics: PrometheusMetricsConfig; + metrics: PrometheusMetricsConfig; /** Path segment for REST API endpoints. */ @Env('N8N_ENDPOINT_REST') - readonly rest: string = 'rest'; + rest = 'rest'; /** Path segment for form endpoints. */ @Env('N8N_ENDPOINT_FORM') - readonly form: string = 'form'; + form = 'form'; /** Path segment for test form endpoints. */ @Env('N8N_ENDPOINT_FORM_TEST') - readonly formTest: string = 'form-test'; + formTest = 'form-test'; /** Path segment for waiting form endpoints. */ @Env('N8N_ENDPOINT_FORM_WAIT') - readonly formWaiting: string = 'form-waiting'; + formWaiting = 'form-waiting'; /** Path segment for webhook endpoints. */ @Env('N8N_ENDPOINT_WEBHOOK') - readonly webhook: string = 'webhook'; + webhook = 'webhook'; /** Path segment for test webhook endpoints. */ @Env('N8N_ENDPOINT_WEBHOOK_TEST') - readonly webhookTest: string = 'webhook-test'; + webhookTest = 'webhook-test'; /** Path segment for waiting webhook endpoints. */ @Env('N8N_ENDPOINT_WEBHOOK_WAIT') - readonly webhookWaiting: string = 'webhook-waiting'; + webhookWaiting = 'webhook-waiting'; /** Whether to disable n8n's UI (frontend). */ @Env('N8N_DISABLE_UI') - readonly disableUi: boolean = false; + disableUi = false; /** Whether to disable production webhooks on the main process, when using webhook-specific processes. */ @Env('N8N_DISABLE_PRODUCTION_MAIN_PROCESS') - readonly disableProductionWebhooksOnMainProcess: boolean = false; + disableProductionWebhooksOnMainProcess = false; /** Colon-delimited list of additional endpoints to not open the UI on. */ @Env('N8N_ADDITIONAL_NON_UI_ROUTES') - readonly additionalNonUIRoutes: string = ''; + additionalNonUIRoutes = ''; } diff --git a/packages/@n8n/config/src/configs/event-bus.ts b/packages/@n8n/config/src/configs/event-bus.ts index ed1226fa923ac..87db613e63968 100644 --- a/packages/@n8n/config/src/configs/event-bus.ts +++ b/packages/@n8n/config/src/configs/event-bus.ts @@ -2,30 +2,30 @@ import { Config, Env, Nested } from '../decorators'; @Config class LogWriterConfig { - /** Number of event log files to keep */ + /* of event log files to keep */ @Env('N8N_EVENTBUS_LOGWRITER_KEEPLOGCOUNT') - readonly keepLogCount: number = 3; + keepLogCount = 3; /** Max size (in KB) of an event log file before a new one is started */ @Env('N8N_EVENTBUS_LOGWRITER_MAXFILESIZEINKB') - readonly maxFileSizeInKB: number = 10240; // 10 MB + maxFileSizeInKB = 10240; // 10 MB /** Basename of event log file */ @Env('N8N_EVENTBUS_LOGWRITER_LOGBASENAME') - readonly logBaseName: string = 'n8nEventLog'; + logBaseName = 'n8nEventLog'; } @Config export class EventBusConfig { /** How often (in ms) to check for unsent event messages. Can in rare cases cause a message to be sent twice. `0` to disable */ @Env('N8N_EVENTBUS_CHECKUNSENTINTERVAL') - readonly checkUnsentInterval: number = 0; + checkUnsentInterval = 0; /** Endpoint to retrieve n8n version information from */ @Nested - readonly logWriter: LogWriterConfig; + logWriter: LogWriterConfig; /** Whether to recover execution details after a crash or only mark status executions as crashed. */ @Env('N8N_EVENTBUS_RECOVERY_MODE') - readonly crashRecoveryMode: 'simple' | 'extensive' = 'extensive'; + crashRecoveryMode: 'simple' | 'extensive' = 'extensive'; } diff --git a/packages/@n8n/config/src/configs/external-secrets.ts b/packages/@n8n/config/src/configs/external-secrets.ts index a5310d675e373..2e51be87bc6c0 100644 --- a/packages/@n8n/config/src/configs/external-secrets.ts +++ b/packages/@n8n/config/src/configs/external-secrets.ts @@ -4,9 +4,9 @@ import { Config, Env } from '../decorators'; export class ExternalSecretsConfig { /** How often (in seconds) to check for secret updates */ @Env('N8N_EXTERNAL_SECRETS_UPDATE_INTERVAL') - readonly updateInterval: number = 300; + updateInterval = 300; /** Whether to prefer GET over LIST when fetching secrets from Hashicorp Vault */ @Env('N8N_EXTERNAL_SECRETS_PREFER_GET') - readonly preferGet: boolean = false; + preferGet = false; } diff --git a/packages/@n8n/config/src/configs/external-storage.ts b/packages/@n8n/config/src/configs/external-storage.ts index c876e0ee34465..3dd1448b44e38 100644 --- a/packages/@n8n/config/src/configs/external-storage.ts +++ b/packages/@n8n/config/src/configs/external-storage.ts @@ -4,39 +4,39 @@ import { Config, Env, Nested } from '../decorators'; class S3BucketConfig { /** Name of the n8n bucket in S3-compatible external storage */ @Env('N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME') - readonly name: string = ''; + name = ''; /** Region of the n8n bucket in S3-compatible external storage @example "us-east-1" */ @Env('N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION') - readonly region: string = ''; + region = ''; } @Config class S3CredentialsConfig { /** Access key in S3-compatible external storage */ @Env('N8N_EXTERNAL_STORAGE_S3_ACCESS_KEY') - readonly accessKey: string = ''; + accessKey = ''; /** Access secret in S3-compatible external storage */ @Env('N8N_EXTERNAL_STORAGE_S3_ACCESS_SECRET') - readonly accessSecret: string = ''; + accessSecret = ''; } @Config class S3Config { /** Host of the n8n bucket in S3-compatible external storage @example "s3.us-east-1.amazonaws.com" */ @Env('N8N_EXTERNAL_STORAGE_S3_HOST') - readonly host: string = ''; + host = ''; @Nested - readonly bucket: S3BucketConfig; + bucket: S3BucketConfig; @Nested - readonly credentials: S3CredentialsConfig; + credentials: S3CredentialsConfig; } @Config export class ExternalStorageConfig { @Nested - readonly s3: S3Config; + s3: S3Config; } diff --git a/packages/@n8n/config/src/configs/nodes.ts b/packages/@n8n/config/src/configs/nodes.ts index f845607a8c0f0..3837b0a99b258 100644 --- a/packages/@n8n/config/src/configs/nodes.ts +++ b/packages/@n8n/config/src/configs/nodes.ts @@ -25,22 +25,26 @@ class CommunityPackagesConfig { /** Whether to enable community packages */ @Env('N8N_COMMUNITY_PACKAGES_ENABLED') enabled: boolean = true; + + /** Whether to reinstall any missing community packages */ + @Env('N8N_REINSTALL_MISSING_PACKAGES') + reinstallMissing: boolean = false; } @Config export class NodesConfig { /** Node types to load. Includes all if unspecified. @example '["n8n-nodes-base.hackerNews"]' */ @Env('NODES_INCLUDE') - readonly include: JsonStringArray = []; + include: JsonStringArray = []; /** Node types not to load. Excludes none if unspecified. @example '["n8n-nodes-base.hackerNews"]' */ @Env('NODES_EXCLUDE') - readonly exclude: JsonStringArray = []; + exclude: JsonStringArray = []; /** Node type to use as error trigger */ @Env('NODES_ERROR_TRIGGER_TYPE') - readonly errorTriggerType: string = 'n8n-nodes-base.errorTrigger'; + errorTriggerType = 'n8n-nodes-base.errorTrigger'; @Nested - readonly communityPackages: CommunityPackagesConfig; + communityPackages: CommunityPackagesConfig; } diff --git a/packages/@n8n/config/src/configs/public-api.ts b/packages/@n8n/config/src/configs/public-api.ts index 33e3bf3fc38bc..b62cac68c7834 100644 --- a/packages/@n8n/config/src/configs/public-api.ts +++ b/packages/@n8n/config/src/configs/public-api.ts @@ -4,13 +4,13 @@ import { Config, Env } from '../decorators'; export class PublicApiConfig { /** Whether to disable the Public API */ @Env('N8N_PUBLIC_API_DISABLED') - readonly disabled: boolean = false; + disabled = false; /** Path segment for the Public API */ @Env('N8N_PUBLIC_API_ENDPOINT') - readonly path: string = 'api'; + path = 'api'; /** Whether to disable the Swagger UI for the Public API */ @Env('N8N_PUBLIC_API_SWAGGERUI_DISABLED') - readonly swaggerUiDisabled: boolean = false; + swaggerUiDisabled = false; } diff --git a/packages/@n8n/config/src/configs/templates.ts b/packages/@n8n/config/src/configs/templates.ts index 3e10c892b3469..3b05048b36301 100644 --- a/packages/@n8n/config/src/configs/templates.ts +++ b/packages/@n8n/config/src/configs/templates.ts @@ -4,9 +4,9 @@ import { Config, Env } from '../decorators'; export class TemplatesConfig { /** Whether to load workflow templates. */ @Env('N8N_TEMPLATES_ENABLED') - readonly enabled: boolean = true; + enabled = true; /** Host to retrieve workflow templates from endpoints. */ @Env('N8N_TEMPLATES_HOST') - readonly host: string = 'https://api.n8n.io/api/'; + host = 'https://api.n8n.io/api/'; } diff --git a/packages/@n8n/config/src/configs/version-notifications.ts b/packages/@n8n/config/src/configs/version-notifications.ts index 1aa693228d494..5fe495ed6c02e 100644 --- a/packages/@n8n/config/src/configs/version-notifications.ts +++ b/packages/@n8n/config/src/configs/version-notifications.ts @@ -4,13 +4,13 @@ import { Config, Env } from '../decorators'; export class VersionNotificationsConfig { /** Whether to request notifications about new n8n versions */ @Env('N8N_VERSION_NOTIFICATIONS_ENABLED') - readonly enabled: boolean = true; + enabled = true; /** Endpoint to retrieve n8n version information from */ @Env('N8N_VERSION_NOTIFICATIONS_ENDPOINT') - readonly endpoint: string = 'https://api.n8n.io/api/versions/'; + endpoint = 'https://api.n8n.io/api/versions/'; /** URL for versions panel to page instructing user on how to update n8n instance */ @Env('N8N_VERSION_NOTIFICATIONS_INFO_URL') - readonly infoUrl: string = 'https://docs.n8n.io/hosting/installation/updating/'; + infoUrl = 'https://docs.n8n.io/hosting/installation/updating/'; } diff --git a/packages/@n8n/config/src/configs/workflows.ts b/packages/@n8n/config/src/configs/workflows.ts index b19f4bc95d7e0..9ca004c886b58 100644 --- a/packages/@n8n/config/src/configs/workflows.ts +++ b/packages/@n8n/config/src/configs/workflows.ts @@ -4,17 +4,14 @@ import { Config, Env } from '../decorators'; export class WorkflowsConfig { /** Default name for workflow */ @Env('WORKFLOWS_DEFAULT_NAME') - readonly defaultName: string = 'My workflow'; + defaultName = 'My workflow'; /** Show onboarding flow in new workflow */ @Env('N8N_ONBOARDING_FLOW_DISABLED') - readonly onboardingFlowDisabled: boolean = false; + onboardingFlowDisabled = false; /** Default option for which workflows may call the current workflow */ @Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION') - readonly callerPolicyDefaultOption: - | 'any' - | 'none' - | 'workflowsFromAList' - | 'workflowsFromSameOwner' = 'workflowsFromSameOwner'; + callerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList' | 'workflowsFromSameOwner' = + 'workflowsFromSameOwner'; } diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index d7bb09889d5ab..33d3e676550ba 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -11,6 +11,7 @@ import { NodesConfig } from './configs/nodes'; import { ExternalStorageConfig } from './configs/external-storage'; import { WorkflowsConfig } from './configs/workflows'; import { EndpointsConfig } from './configs/endpoints'; +import { CacheConfig } from './configs/cache'; @Config class UserManagementConfig { @@ -75,4 +76,7 @@ export class GlobalConfig { @Nested readonly endpoints: EndpointsConfig; + + @Nested + readonly cache: CacheConfig; } diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index a36a74d1e25c7..e077e233b7494 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -108,6 +108,7 @@ describe('GlobalConfig', () => { nodes: { communityPackages: { enabled: true, + reinstallMissing: false, }, errorTriggerType: 'n8n-nodes-base.errorTrigger', include: [], @@ -172,6 +173,17 @@ describe('GlobalConfig', () => { webhookTest: 'webhook-test', webhookWaiting: 'webhook-waiting', }, + cache: { + backend: 'auto', + memory: { + maxSize: 3145728, + ttl: 3600000, + }, + redis: { + prefix: 'redis', + ttl: 3600000, + }, + }, }; it('should use all default values when no env variables are defined', () => { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts index 241efadb6d392..ecc14e1344341 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts @@ -74,7 +74,7 @@ export class LmChatAnthropic implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOllama/LmChatOllama.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOllama/LmChatOllama.node.ts index f0f9e5f630e29..b4fc474dd2ecf 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOllama/LmChatOllama.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOllama/LmChatOllama.node.ts @@ -28,7 +28,7 @@ export class LmChatOllama implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts index 264c594d1bd87..1f39c082290e4 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts @@ -26,7 +26,7 @@ export class LmChatOpenAi implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMCohere/LmCohere.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMCohere/LmCohere.node.ts index ee5163a78f1a3..191209bb3363b 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMCohere/LmCohere.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMCohere/LmCohere.node.ts @@ -26,7 +26,7 @@ export class LmCohere implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMOllama/LmOllama.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMOllama/LmOllama.node.ts index 95024ad4b2f39..5492a51a97914 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMOllama/LmOllama.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMOllama/LmOllama.node.ts @@ -27,7 +27,7 @@ export class LmOllama implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMOpenAi/LmOpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMOpenAi/LmOpenAi.node.ts index 2f0e3480d8701..a46ad429a2963 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMOpenAi/LmOpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMOpenAi/LmOpenAi.node.ts @@ -38,7 +38,7 @@ export class LmOpenAi implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.ts index 9a30ef74d73ae..7b2c821f9c34c 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.ts @@ -26,7 +26,7 @@ export class LmOpenHuggingFaceInference implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts index d313ba53b54e4..4e1c27bfde8c2 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts @@ -29,7 +29,7 @@ export class LmChatAwsBedrock implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.ts index 2e05b4770d99d..5770387158b1e 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.ts @@ -27,7 +27,7 @@ export class LmChatAzureOpenAi implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts index d3d3d1ea2c218..ce08a650f203f 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts @@ -27,7 +27,7 @@ export class LmChatGoogleGemini implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts index a32a5e959cdb5..6195d4d98763b 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts @@ -25,7 +25,7 @@ export class LmChatGooglePalm implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts index 1e3837818fb5b..044428c01adc3 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts @@ -32,7 +32,7 @@ export class LmChatGoogleVertex implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts index 3354ac030f404..d0a28715e1010 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts @@ -26,7 +26,7 @@ export class LmChatGroq implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts index 32364545d2073..129beeadfe5eb 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts @@ -27,7 +27,7 @@ export class LmChatMistralCloud implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts index d79f74be02fe6..e4681803fe59a 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts @@ -25,7 +25,7 @@ export class LmGooglePalm implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/cli/.eslintrc.js b/packages/cli/.eslintrc.js index bb1041607ddcd..17ecfee499ae9 100644 --- a/packages/cli/.eslintrc.js +++ b/packages/cli/.eslintrc.js @@ -40,13 +40,13 @@ module.exports = { overrides: [ { - files: ['./src/databases/**/*.ts', './test/**/*.ts'], + files: ['./src/databases/**/*.ts', './test/**/*.ts', './src/**/__tests__/**/*.ts'], rules: { 'n8n-local-rules/misplaced-n8n-typeorm-import': 'off', }, }, { - files: ['./test/**/*.ts'], + files: ['./test/**/*.ts', './src/**/__tests__/**/*.ts'], rules: { 'n8n-local-rules/no-type-unsafe-event-emitter': 'off', }, diff --git a/packages/cli/src/ActiveWebhooks.ts b/packages/cli/src/ActiveWebhooks.ts index 79626df025f75..43136e75d384a 100644 --- a/packages/cli/src/ActiveWebhooks.ts +++ b/packages/cli/src/ActiveWebhooks.ts @@ -72,11 +72,9 @@ export class ActiveWebhooks implements IWebhookManager { const pathElements = path.split('/').slice(1); // extracting params from path - // @ts-ignore webhook.webhookPath.split('/').forEach((ele, index) => { if (ele.startsWith(':')) { // write params to req.params - // @ts-ignore request.params[ele.slice(1)] = pathElements[index]; } }); diff --git a/packages/cli/test/unit/ExternalSecrets/ExternalSecretsManager.test.ts b/packages/cli/src/ExternalSecrets/__tests__/ExternalSecretsManager.ee.test.ts similarity index 97% rename from packages/cli/test/unit/ExternalSecrets/ExternalSecretsManager.test.ts rename to packages/cli/src/ExternalSecrets/__tests__/ExternalSecretsManager.ee.test.ts index cf72688d24397..92475c93115e9 100644 --- a/packages/cli/test/unit/ExternalSecrets/ExternalSecretsManager.test.ts +++ b/packages/cli/src/ExternalSecrets/__tests__/ExternalSecretsManager.ee.test.ts @@ -6,13 +6,13 @@ import { License } from '@/License'; import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee'; import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee'; import { InternalHooks } from '@/InternalHooks'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { DummyProvider, ErrorProvider, FailedProvider, MockProviders, -} from '../../shared/ExternalSecrets/utils'; +} from '@test/ExternalSecrets/utils'; import { mock } from 'jest-mock-extended'; describe('External Secrets Manager', () => { diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 25f758185aea9..8ddbd10fb29c1 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -267,14 +267,6 @@ export interface IWebhookManager { executeWebhook(req: WebhookRequest, res: Response): Promise; } -export interface ITelemetryUserDeletionData { - user_id: string; - target_user_old_status: 'active' | 'invited'; - migration_strategy?: 'transfer_data' | 'delete_data'; - target_user_id?: string; - migration_user_id?: string; -} - export interface IVersionNotificationSettings { enabled: boolean; endpoint: string; diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index af72d6bfece78..031a7fe3a6259 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -1,9 +1,6 @@ import { Service } from 'typedi'; -import { snakeCase } from 'change-case'; import type { ITelemetryTrackProperties } from 'n8n-workflow'; -import type { AuthProviderType } from '@db/entities/AuthIdentity'; import type { User } from '@db/entities/User'; -import type { ITelemetryUserDeletionData } from '@/Interfaces'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; import { Telemetry } from '@/telemetry'; import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus'; @@ -39,16 +36,6 @@ export class InternalHooks { this.telemetry.track('Session started', { session_id: pushRef }); } - onPersonalizationSurveySubmitted(userId: string, answers: Record): void { - const camelCaseKeys = Object.keys(answers); - const personalizationSurveyData = { user_id: userId } as Record; - camelCaseKeys.forEach((camelCaseKey) => { - personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey]; - }); - - this.telemetry.track('User responded to personalization questions', personalizationSurveyData); - } - onWorkflowSharingUpdate(workflowId: string, userId: string, userList: string[]) { const properties: ITelemetryTrackProperties = { workflow_id: workflowId, @@ -67,76 +54,6 @@ export class InternalHooks { return await Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]); } - onUserDeletion(userDeletionData: { - user: User; - telemetryData: ITelemetryUserDeletionData; - publicApi: boolean; - }) { - this.telemetry.track('User deleted user', { - ...userDeletionData.telemetryData, - user_id: userDeletionData.user.id, - public_api: userDeletionData.publicApi, - }); - } - - onUserInvite(userInviteData: { - user: User; - target_user_id: string[]; - public_api: boolean; - email_sent: boolean; - invitee_role: string; - }) { - this.telemetry.track('User invited new user', { - user_id: userInviteData.user.id, - target_user_id: userInviteData.target_user_id, - public_api: userInviteData.public_api, - email_sent: userInviteData.email_sent, - invitee_role: userInviteData.invitee_role, - }); - } - - onUserRoleChange(userRoleChangeData: { - user: User; - target_user_id: string; - public_api: boolean; - target_user_new_role: string; - }) { - const { user, ...rest } = userRoleChangeData; - - this.telemetry.track('User changed role', { user_id: user.id, ...rest }); - } - - onUserRetrievedUser(userRetrievedData: { user_id: string; public_api: boolean }) { - this.telemetry.track('User retrieved user', userRetrievedData); - } - - onUserRetrievedAllUsers(userRetrievedData: { user_id: string; public_api: boolean }) { - this.telemetry.track('User retrieved all users', userRetrievedData); - } - - onUserRetrievedExecution(userRetrievedData: { user_id: string; public_api: boolean }) { - this.telemetry.track('User retrieved execution', userRetrievedData); - } - - onUserRetrievedAllExecutions(userRetrievedData: { user_id: string; public_api: boolean }) { - this.telemetry.track('User retrieved all executions', userRetrievedData); - } - - onUserRetrievedWorkflow(userRetrievedData: { user_id: string; public_api: boolean }) { - this.telemetry.track('User retrieved workflow', userRetrievedData); - } - - onUserRetrievedAllWorkflows(userRetrievedData: { user_id: string; public_api: boolean }) { - this.telemetry.track('User retrieved all workflows', userRetrievedData); - } - - onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }) { - this.telemetry.track('User changed personal settings', { - user_id: userUpdateData.user.id, - fields_changed: userUpdateData.fields_changed, - }); - } - onUserInviteEmailClick(userInviteClickData: { inviter: User; invitee: User }) { this.telemetry.track('User clicked invite link from email', { user_id: userInviteClickData.invitee.id, @@ -172,19 +89,6 @@ export class InternalHooks { this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData); } - onUserSignup( - user: User, - userSignupData: { - user_type: AuthProviderType; - was_disabled_ldap_user: boolean; - }, - ) { - this.telemetry.track('User signed up', { - user_id: user.id, - ...userSignupData, - }); - } - onEmailFailed(failedEmailData: { user: User; message_type: diff --git a/packages/cli/test/unit/Ldap/helpers.test.ts b/packages/cli/src/Ldap/__tests__/helpers.test.ts similarity index 96% rename from packages/cli/test/unit/Ldap/helpers.test.ts rename to packages/cli/src/Ldap/__tests__/helpers.test.ts index debb96da382d1..719adea76c5a7 100644 --- a/packages/cli/test/unit/Ldap/helpers.test.ts +++ b/packages/cli/src/Ldap/__tests__/helpers.test.ts @@ -1,5 +1,5 @@ import { UserRepository } from '@/databases/repositories/user.repository'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import * as helpers from '@/Ldap/helpers.ee'; import { AuthIdentity } from '@/databases/entities/AuthIdentity'; import { User } from '@/databases/entities/User'; diff --git a/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts b/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts index ed078b52d5249..ab6927724c1aa 100644 --- a/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts @@ -7,7 +7,7 @@ import { validCursor } from '../../shared/middlewares/global.middleware'; import type { ExecutionRequest } from '../../../types'; import { getSharedWorkflowIds } from '../workflows/workflows.service'; import { encodeNextCursor } from '../../shared/services/pagination.service'; -import { InternalHooks } from '@/InternalHooks'; +import { EventService } from '@/events/event.service'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; @@ -78,9 +78,9 @@ export = { return res.status(404).json({ message: 'Not Found' }); } - Container.get(InternalHooks).onUserRetrievedExecution({ - user_id: req.user.id, - public_api: true, + Container.get(EventService).emit('user-retrieved-execution', { + userId: req.user.id, + publicApi: true, }); return res.json(replaceCircularReferences(execution)); @@ -130,9 +130,9 @@ export = { const count = await Container.get(ExecutionRepository).getExecutionsCountForPublicApi(filters); - Container.get(InternalHooks).onUserRetrievedAllExecutions({ - user_id: req.user.id, - public_api: true, + Container.get(EventService).emit('user-retrieved-all-executions', { + userId: req.user.id, + publicApi: true, }); return res.json({ diff --git a/packages/cli/src/PublicApi/v1/handlers/users/users.handler.ee.ts b/packages/cli/src/PublicApi/v1/handlers/users/users.handler.ee.ts index 53c568ee69acd..59a7a4c3e3f42 100644 --- a/packages/cli/src/PublicApi/v1/handlers/users/users.handler.ee.ts +++ b/packages/cli/src/PublicApi/v1/handlers/users/users.handler.ee.ts @@ -11,7 +11,7 @@ import { validLicenseWithUserQuota, } from '../../shared/middlewares/global.middleware'; import type { UserRequest } from '@/requests'; -import { InternalHooks } from '@/InternalHooks'; +import { EventService } from '@/events/event.service'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; import type { Response } from 'express'; import { InvitationController } from '@/controllers/invitation.controller'; @@ -37,12 +37,10 @@ export = { }); } - const telemetryData = { - user_id: req.user.id, - public_api: true, - }; - - Container.get(InternalHooks).onUserRetrievedUser(telemetryData); + Container.get(EventService).emit('user-retrieved-user', { + userId: req.user.id, + publicApi: true, + }); return res.json(clean(user, { includeRole })); }, @@ -65,12 +63,10 @@ export = { in: _in, }); - const telemetryData = { - user_id: req.user.id, - public_api: true, - }; - - Container.get(InternalHooks).onUserRetrievedAllUsers(telemetryData); + Container.get(EventService).emit('user-retrieved-all-users', { + userId: req.user.id, + publicApi: true, + }); return res.json({ data: clean(users, { includeRole }), diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts index 16434ef4e9a1e..d5abac95a1787 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts @@ -26,7 +26,6 @@ import { updateTags, } from './workflows.service'; import { WorkflowService } from '@/workflows/workflow.service'; -import { InternalHooks } from '@/InternalHooks'; import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee'; import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository'; import { TagRepository } from '@/databases/repositories/tag.repository'; @@ -119,9 +118,9 @@ export = { return res.status(404).json({ message: 'Not Found' }); } - Container.get(InternalHooks).onUserRetrievedWorkflow({ - user_id: req.user.id, - public_api: true, + Container.get(EventService).emit('user-retrieved-workflow', { + userId: req.user.id, + publicApi: true, }); return res.json(workflow); @@ -185,9 +184,9 @@ export = { ...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }), }); - Container.get(InternalHooks).onUserRetrievedAllWorkflows({ - user_id: req.user.id, - public_api: true, + Container.get(EventService).emit('user-retrieved-all-workflows', { + userId: req.user.id, + publicApi: true, }); return res.json({ diff --git a/packages/cli/test/unit/UserManagementMailer.test.ts b/packages/cli/src/UserManagement/email/__tests__/UserManagementMailer.test.ts similarity index 100% rename from packages/cli/test/unit/UserManagementMailer.test.ts rename to packages/cli/src/UserManagement/email/__tests__/UserManagementMailer.test.ts diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 74c469284a7eb..127e0cc817393 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -717,7 +717,6 @@ export async function getRunData( const runData: IWorkflowExecutionDataProcess = { executionMode: mode, executionData: runExecutionData, - // @ts-ignore workflowData, }; @@ -1000,10 +999,10 @@ export async function getBase( executeWorkflow, restApiUrl: urlBaseWebhook + globalConfig.endpoints.rest, instanceBaseUrl: urlBaseWebhook, - formWaitingBaseUrl: globalConfig.endpoints.formWaiting, - webhookBaseUrl: globalConfig.endpoints.webhook, - webhookWaitingBaseUrl: globalConfig.endpoints.webhookWaiting, - webhookTestBaseUrl: globalConfig.endpoints.webhookTest, + formWaitingBaseUrl: urlBaseWebhook + globalConfig.endpoints.formWaiting, + webhookBaseUrl: urlBaseWebhook + globalConfig.endpoints.webhook, + webhookWaitingBaseUrl: urlBaseWebhook + globalConfig.endpoints.webhookWaiting, + webhookTestBaseUrl: urlBaseWebhook + globalConfig.endpoints.webhookTest, currentNodeParameters, executionTimeoutTimestamp, userId, diff --git a/packages/cli/test/unit/ActiveExecutions.test.ts b/packages/cli/src/__tests__/ActiveExecutions.test.ts similarity index 100% rename from packages/cli/test/unit/ActiveExecutions.test.ts rename to packages/cli/src/__tests__/ActiveExecutions.test.ts diff --git a/packages/cli/test/unit/CredentialTypes.test.ts b/packages/cli/src/__tests__/CredentialTypes.test.ts similarity index 95% rename from packages/cli/test/unit/CredentialTypes.test.ts rename to packages/cli/src/__tests__/CredentialTypes.test.ts index 0ee99c9f8ae6e..2a8e6a939a045 100644 --- a/packages/cli/test/unit/CredentialTypes.test.ts +++ b/packages/cli/src/__tests__/CredentialTypes.test.ts @@ -1,7 +1,7 @@ import { CredentialTypes } from '@/CredentialTypes'; import { Container } from 'typedi'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; describe('CredentialTypes', () => { const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, { diff --git a/packages/cli/test/unit/CredentialsHelper.test.ts b/packages/cli/src/__tests__/CredentialsHelper.test.ts similarity index 99% rename from packages/cli/test/unit/CredentialsHelper.test.ts rename to packages/cli/src/__tests__/CredentialsHelper.test.ts index 19be100b02b84..88cd19ad3dc5d 100644 --- a/packages/cli/test/unit/CredentialsHelper.test.ts +++ b/packages/cli/src/__tests__/CredentialsHelper.test.ts @@ -14,7 +14,7 @@ import { NodeTypes } from '@/NodeTypes'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; describe('CredentialsHelper', () => { mockInstance(CredentialsRepository); diff --git a/packages/cli/test/unit/License.test.ts b/packages/cli/src/__tests__/License.test.ts similarity index 99% rename from packages/cli/test/unit/License.test.ts rename to packages/cli/src/__tests__/License.test.ts index 6d7311656c961..31effecc0eba4 100644 --- a/packages/cli/test/unit/License.test.ts +++ b/packages/cli/src/__tests__/License.test.ts @@ -5,7 +5,7 @@ import config from '@/config'; import { License } from '@/License'; import { Logger } from '@/Logger'; import { N8N_VERSION } from '@/constants'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { OrchestrationService } from '@/services/orchestration.service'; jest.mock('@n8n_io/license-sdk'); diff --git a/packages/cli/test/unit/TestWebhooks.test.ts b/packages/cli/src/__tests__/TestWebhooks.test.ts similarity index 100% rename from packages/cli/test/unit/TestWebhooks.test.ts rename to packages/cli/src/__tests__/TestWebhooks.test.ts diff --git a/packages/cli/test/unit/WaitTracker.test.ts b/packages/cli/src/__tests__/WaitTracker.test.ts similarity index 100% rename from packages/cli/test/unit/WaitTracker.test.ts rename to packages/cli/src/__tests__/WaitTracker.test.ts diff --git a/packages/cli/test/unit/WebhookHelpers.test.ts b/packages/cli/src/__tests__/WebhookHelpers.test.ts similarity index 100% rename from packages/cli/test/unit/WebhookHelpers.test.ts rename to packages/cli/src/__tests__/WebhookHelpers.test.ts diff --git a/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts b/packages/cli/src/__tests__/WorkflowExecuteAdditionalData.test.ts similarity index 96% rename from packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts rename to packages/cli/src/__tests__/WorkflowExecuteAdditionalData.test.ts index 2984220637aae..eca60e56c5216 100644 --- a/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts +++ b/packages/cli/src/__tests__/WorkflowExecuteAdditionalData.test.ts @@ -1,5 +1,5 @@ import { VariablesService } from '@/environments/variables/variables.service.ee'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; import { getBase } from '@/WorkflowExecuteAdditionalData'; import Container from 'typedi'; diff --git a/packages/cli/test/unit/WorkflowHelpers.test.ts b/packages/cli/src/__tests__/WorkflowHelpers.test.ts similarity index 100% rename from packages/cli/test/unit/WorkflowHelpers.test.ts rename to packages/cli/src/__tests__/WorkflowHelpers.test.ts diff --git a/packages/cli/test/unit/WorkflowRunner.test.ts b/packages/cli/src/__tests__/WorkflowRunner.test.ts similarity index 86% rename from packages/cli/test/unit/WorkflowRunner.test.ts rename to packages/cli/src/__tests__/WorkflowRunner.test.ts index c972d6bb73933..668150092f60a 100644 --- a/packages/cli/test/unit/WorkflowRunner.test.ts +++ b/packages/cli/src/__tests__/WorkflowRunner.test.ts @@ -4,11 +4,11 @@ import type { User } from '@db/entities/User'; import { WorkflowRunner } from '@/WorkflowRunner'; import config from '@/config'; -import * as testDb from '../integration/shared/testDb'; -import { setupTestServer } from '../integration/shared/utils'; -import { createUser } from '../integration/shared/db/users'; -import { createWorkflow } from '../integration/shared/db/workflows'; -import { createExecution } from '../integration/shared/db/executions'; +import * as testDb from '@test-integration/testDb'; +import { setupTestServer } from '@test-integration/utils'; +import { createUser } from '@test-integration/db/users'; +import { createWorkflow } from '@test-integration/db/workflows'; +import { createExecution } from '@test-integration/db/executions'; import { mockInstance } from '@test/mocking'; import { Telemetry } from '@/telemetry'; diff --git a/packages/cli/test/unit/auth/auth.service.test.ts b/packages/cli/src/auth/__tests__/auth.service.test.ts similarity index 100% rename from packages/cli/test/unit/auth/auth.service.test.ts rename to packages/cli/src/auth/__tests__/auth.service.test.ts diff --git a/packages/cli/src/auth/methods/ldap.ts b/packages/cli/src/auth/methods/ldap.ts index c632557c95c85..66f6f6dcd9ffe 100644 --- a/packages/cli/src/auth/methods/ldap.ts +++ b/packages/cli/src/auth/methods/ldap.ts @@ -1,6 +1,5 @@ import { Container } from 'typedi'; -import { InternalHooks } from '@/InternalHooks'; import { LdapService } from '@/Ldap/ldap.service.ee'; import { createLdapUserOnLocalDb, @@ -51,11 +50,11 @@ export const handleLdapLogin = async ( await updateLdapUserOnLocalDb(identity, ldapAttributesValues); } else { const user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId); - Container.get(InternalHooks).onUserSignup(user, { - user_type: 'ldap', - was_disabled_ldap_user: false, + Container.get(EventService).emit('user-signed-up', { + user, + userType: 'ldap', + wasDisabledLdapUser: false, }); - Container.get(EventService).emit('user-signed-up', { user }); return user; } } else { diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index 9828e6207b525..62ff002e43298 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -44,13 +44,16 @@ export abstract class BaseCommand extends Command { protected license: License; - protected globalConfig = Container.get(GlobalConfig); + protected readonly globalConfig = Container.get(GlobalConfig); /** * How long to wait for graceful shutdown before force killing the process. */ protected gracefulShutdownTimeoutInS = config.getEnv('generic.gracefulShutdownTimeout'); + /** Whether to init community packages (if enabled) */ + protected needsCommunityPackages = false; + async init(): Promise { await initErrorHandling(); initExpressionEvaluator(); @@ -111,6 +114,12 @@ export abstract class BaseCommand extends Command { ); } + const { communityPackages } = this.globalConfig.nodes; + if (communityPackages.enabled && this.needsCommunityPackages) { + const { CommunityPackagesService } = await import('@/services/communityPackages.service'); + await Container.get(CommunityPackagesService).checkForMissingPackages(); + } + await Container.get(PostHogClient).init(); await Container.get(InternalHooks).init(); await Container.get(TelemetryEventRelay).init(); diff --git a/packages/cli/test/unit/commands/db/revert.test.ts b/packages/cli/src/commands/db/__tests__/revert.test.ts similarity index 98% rename from packages/cli/test/unit/commands/db/revert.test.ts rename to packages/cli/src/commands/db/__tests__/revert.test.ts index cea3f89253383..13c554a786ad3 100644 --- a/packages/cli/test/unit/commands/db/revert.test.ts +++ b/packages/cli/src/commands/db/__tests__/revert.test.ts @@ -1,5 +1,5 @@ import { main } from '@/commands/db/revert'; -import { mockInstance } from '../../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { Logger } from '@/Logger'; import type { IrreversibleMigration, ReversibleMigration } from '@/databases/types'; import type { Migration, MigrationExecutor } from '@n8n/typeorm'; diff --git a/packages/cli/src/commands/execute.ts b/packages/cli/src/commands/execute.ts index a375d19c31034..cdf949e87ceda 100644 --- a/packages/cli/src/commands/execute.ts +++ b/packages/cli/src/commands/execute.ts @@ -27,6 +27,8 @@ export class Execute extends BaseCommand { }), }; + override needsCommunityPackages = true; + async init() { await super.init(); await this.initBinaryDataService(); diff --git a/packages/cli/src/commands/executeBatch.ts b/packages/cli/src/commands/executeBatch.ts index d8fdbeb8a201c..227dd962efb1b 100644 --- a/packages/cli/src/commands/executeBatch.ts +++ b/packages/cli/src/commands/executeBatch.ts @@ -108,6 +108,8 @@ export class ExecuteBatch extends BaseCommand { }), }; + override needsCommunityPackages = true; + /** * Gracefully handles exit. * @param {boolean} skipExit Whether to skip exit or number according to received signal diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 76d8b4c7e9273..98e8e5b98141c 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -8,7 +8,6 @@ import { createReadStream, createWriteStream, existsSync } from 'fs'; import { pipeline } from 'stream/promises'; import replaceStream from 'replacestream'; import glob from 'fast-glob'; -import { GlobalConfig } from '@n8n/config'; import { jsonParse, randomString } from 'n8n-workflow'; import config from '@/config'; @@ -68,6 +67,8 @@ export class Start extends BaseCommand { protected server = Container.get(Server); + override needsCommunityPackages = true; + constructor(argv: string[], cmdConfig: Config) { super(argv, cmdConfig); this.setInstanceType('main'); @@ -125,7 +126,6 @@ export class Start extends BaseCommand { private async generateStaticAssets() { // Read the index file and replace the path placeholder const n8nPath = this.globalConfig.path; - const hooksUrls = config.getEnv('externalFrontendHooksUrls'); let scriptsString = ''; @@ -178,6 +178,22 @@ export class Start extends BaseCommand { this.logger.debug(`Queue mode id: ${this.queueModeId}`); } + const { flags } = await this.parse(Start); + const { communityPackages } = this.globalConfig.nodes; + // cli flag overrides the config env variable + if (flags.reinstallMissingPackages) { + if (communityPackages.enabled) { + this.logger.warn( + '`--reinstallMissingPackages` is deprecated: Please use the env variable `N8N_REINSTALL_MISSING_PACKAGES` instead', + ); + communityPackages.reinstallMissing = true; + } else { + this.logger.warn( + '`--reinstallMissingPackages` was passed, but community packages are disabled', + ); + } + } + await super.init(); this.activeWorkflowManager = Container.get(ActiveWorkflowManager); @@ -251,18 +267,9 @@ export class Start extends BaseCommand { config.set(setting.key, jsonParse(setting.value, { fallbackValue: setting.value })); }); - const globalConfig = Container.get(GlobalConfig); - - if (globalConfig.nodes.communityPackages.enabled) { - const { CommunityPackagesService } = await import('@/services/communityPackages.service'); - await Container.get(CommunityPackagesService).setMissingPackages({ - reinstallMissingPackages: flags.reinstallMissingPackages, - }); - } - - const { type: dbType } = globalConfig.database; + const { type: dbType } = this.globalConfig.database; if (dbType === 'sqlite') { - const shouldRunVacuum = globalConfig.database.sqlite.executeVacuumOnStartup; + const shouldRunVacuum = this.globalConfig.database.sqlite.executeVacuumOnStartup; if (shouldRunVacuum) { await Container.get(ExecutionRepository).query('VACUUM;'); } @@ -282,7 +289,7 @@ export class Start extends BaseCommand { } const { default: localtunnel } = await import('@n8n/localtunnel'); - const { port } = Container.get(GlobalConfig); + const { port } = this.globalConfig; const webhookTunnel = await localtunnel(port, { host: 'https://hooks.n8n.cloud', diff --git a/packages/cli/src/commands/webhook.ts b/packages/cli/src/commands/webhook.ts index 5b72c1eb86a9b..e76ac358170c1 100644 --- a/packages/cli/src/commands/webhook.ts +++ b/packages/cli/src/commands/webhook.ts @@ -22,6 +22,8 @@ export class Webhook extends BaseCommand { protected server = Container.get(WebhookServer); + override needsCommunityPackages = true; + constructor(argv: string[], cmdConfig: Config) { super(argv, cmdConfig); this.setInstanceType('webhook'); diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index cf4c23a0859a7..151ecfdf90d12 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -3,7 +3,6 @@ import { Flags, type Config } from '@oclif/core'; import express from 'express'; import http from 'http'; import type PCancelable from 'p-cancelable'; -import { GlobalConfig } from '@n8n/config'; import { WorkflowExecute } from 'n8n-core'; import type { ExecutionStatus, IExecuteResponsePromiseData, INodeTypes, IRun } from 'n8n-workflow'; import { Workflow, sleep, ApplicationError } from 'n8n-workflow'; @@ -57,6 +56,8 @@ export class Worker extends BaseCommand { redisSubscriber: RedisServicePubSubSubscriber; + override needsCommunityPackages = true; + /** * Stop n8n in a graceful way. * Make for example sure that all the webhooks from third party services @@ -429,8 +430,7 @@ export class Worker extends BaseCommand { let presetCredentialsLoaded = false; - const globalConfig = Container.get(GlobalConfig); - const endpointPresetCredentials = globalConfig.credentials.overwrite.endpoint; + const endpointPresetCredentials = this.globalConfig.credentials.overwrite.endpoint; if (endpointPresetCredentials !== '') { // POST endpoint to set preset credentials app.post( diff --git a/packages/cli/test/unit/config/index.test.ts b/packages/cli/src/config/__tests__/index.test.ts similarity index 100% rename from packages/cli/test/unit/config/index.test.ts rename to packages/cli/src/config/__tests__/index.test.ts diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index f681fe32eaf15..0f263d6931162 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -660,43 +660,6 @@ export const schema = { }, }, - cache: { - backend: { - doc: 'Backend to use for caching', - format: ['memory', 'redis', 'auto'] as const, - default: 'auto', - env: 'N8N_CACHE_BACKEND', - }, - memory: { - maxSize: { - doc: 'Maximum size of memory cache in bytes', - format: Number, - default: 3 * 1024 * 1024, // 3 MB - env: 'N8N_CACHE_MEMORY_MAX_SIZE', - }, - ttl: { - doc: 'Time to live for cached items in memory (in ms)', - format: Number, - default: 3600 * 1000, // 1 hour - env: 'N8N_CACHE_MEMORY_TTL', - }, - }, - redis: { - prefix: { - doc: 'Prefix for all cache keys', - format: String, - default: 'cache', - env: 'N8N_CACHE_REDIS_KEY_PREFIX', - }, - ttl: { - doc: 'Time to live for cached items in redis (in ms), 0 for no TTL', - format: Number, - default: 3600 * 1000, // 1 hour - env: 'N8N_CACHE_REDIS_TTL', - }, - }, - }, - /** * @important Do not remove until after cloud hooks are updated to stop using convict config. */ diff --git a/packages/cli/test/unit/controllers/curl.controller.test.ts b/packages/cli/src/controllers/__tests__/curl.controller.test.ts similarity index 100% rename from packages/cli/test/unit/controllers/curl.controller.test.ts rename to packages/cli/src/controllers/__tests__/curl.controller.test.ts diff --git a/packages/cli/test/unit/controllers/dynamic-node-parameters.controller.test.ts b/packages/cli/src/controllers/__tests__/dynamic-node-parameters.controller.test.ts similarity index 100% rename from packages/cli/test/unit/controllers/dynamic-node-parameters.controller.test.ts rename to packages/cli/src/controllers/__tests__/dynamic-node-parameters.controller.test.ts diff --git a/packages/cli/test/unit/controllers/me.controller.test.ts b/packages/cli/src/controllers/__tests__/me.controller.test.ts similarity index 93% rename from packages/cli/test/unit/controllers/me.controller.test.ts rename to packages/cli/src/controllers/__tests__/me.controller.test.ts index 2416ecafbddb4..4a313552af497 100644 --- a/packages/cli/test/unit/controllers/me.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/me.controller.test.ts @@ -13,14 +13,16 @@ import { InternalHooks } from '@/InternalHooks'; import { License } from '@/License'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { badPasswords } from '../shared/testData'; -import { mockInstance } from '../../shared/mocking'; +import { EventService } from '@/events/event.service'; +import { badPasswords } from '@test/testData'; +import { mockInstance } from '@test/mocking'; const browserId = 'test-browser-id'; describe('MeController', () => { const externalHooks = mockInstance(ExternalHooks); - const internalHooks = mockInstance(InternalHooks); + mockInstance(InternalHooks); + const eventService = mockInstance(EventService); const userService = mockInstance(UserService); const userRepository = mockInstance(UserRepository); mockInstance(License).isWithinUsersLimit.mockReturnValue(true); @@ -44,6 +46,7 @@ describe('MeController', () => { it('should update the user in the DB, and issue a new cookie', async () => { const user = mock({ id: '123', + email: 'valid@email.com', password: 'password', authIdentities: [], role: 'global:owner', @@ -51,6 +54,7 @@ describe('MeController', () => { const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; const req = mock({ user, body: reqBody, browserId }); const res = mock(); + userRepository.findOneByOrFail.mockResolvedValue(user); userRepository.findOneOrFail.mockResolvedValue(user); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); userService.toPublic.mockResolvedValue({} as unknown as PublicUser); @@ -64,7 +68,10 @@ describe('MeController', () => { ]); expect(userService.update).toHaveBeenCalled(); - + expect(eventService.emit).toHaveBeenCalledWith('user-updated', { + user, + fieldsChanged: ['firstName', 'lastName'], // email did not change + }); expect(res.cookie).toHaveBeenCalledWith( AUTH_COOKIE_NAME, 'signed-token', @@ -202,9 +209,9 @@ describe('MeController', () => { req.user.password, ]); - expect(internalHooks.onUserUpdate).toHaveBeenCalledWith({ + expect(eventService.emit).toHaveBeenCalledWith('user-updated', { user: req.user, - fields_changed: ['password'], + fieldsChanged: ['password'], }); }); }); diff --git a/packages/cli/test/unit/controllers/owner.controller.test.ts b/packages/cli/src/controllers/__tests__/owner.controller.test.ts similarity index 97% rename from packages/cli/test/unit/controllers/owner.controller.test.ts rename to packages/cli/src/controllers/__tests__/owner.controller.test.ts index 0057c2370896b..3aabc87cade9a 100644 --- a/packages/cli/test/unit/controllers/owner.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/owner.controller.test.ts @@ -16,8 +16,8 @@ import type { OwnerRequest } from '@/requests'; import type { UserService } from '@/services/user.service'; import { PasswordUtility } from '@/services/password.utility'; -import { mockInstance } from '../../shared/mocking'; -import { badPasswords } from '../shared/testData'; +import { mockInstance } from '@test/mocking'; +import { badPasswords } from '@test/testData'; describe('OwnerController', () => { const configGetSpy = jest.spyOn(config, 'getEnv'); diff --git a/packages/cli/test/unit/controllers/translation.controller.test.ts b/packages/cli/src/controllers/__tests__/translation.controller.test.ts similarity index 100% rename from packages/cli/test/unit/controllers/translation.controller.test.ts rename to packages/cli/src/controllers/__tests__/translation.controller.test.ts diff --git a/packages/cli/test/unit/controllers/userSettings.controller.test.ts b/packages/cli/src/controllers/__tests__/userSettings.controller.test.ts similarity index 96% rename from packages/cli/test/unit/controllers/userSettings.controller.test.ts rename to packages/cli/src/controllers/__tests__/userSettings.controller.test.ts index e29afb74ccc29..3794008641fa5 100644 --- a/packages/cli/test/unit/controllers/userSettings.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/userSettings.controller.test.ts @@ -60,7 +60,7 @@ describe('UserSettingsController', () => { [], ], [ - 'updates user settings, reseting to waiting state', + 'updates user settings, resetting to waiting state', { waitingForResponse: true, ignoredCount: 0, @@ -137,7 +137,7 @@ describe('UserSettingsController', () => { 'is waitingForResponse but missing ignoredCount', { lastShownAt: 123, waitingForResponse: true }, ], - ])('thows error when request payload is %s', async (_, payload) => { + ])('throws error when request payload is %s', async (_, payload) => { const req = mock(); req.user.id = '1'; req.body = payload; diff --git a/packages/cli/src/controllers/__tests__/users.controller.test.ts b/packages/cli/src/controllers/__tests__/users.controller.test.ts new file mode 100644 index 0000000000000..38a4399cab2ba --- /dev/null +++ b/packages/cli/src/controllers/__tests__/users.controller.test.ts @@ -0,0 +1,52 @@ +import { mock } from 'jest-mock-extended'; +import { UsersController } from '../users.controller'; +import type { UserRequest } from '@/requests'; +import type { EventService } from '@/events/event.service'; +import type { User } from '@/databases/entities/User'; +import type { UserRepository } from '@/databases/repositories/user.repository'; +import type { ProjectService } from '@/services/project.service'; + +describe('UsersController', () => { + const eventService = mock(); + const userRepository = mock(); + const projectService = mock(); + const controller = new UsersController( + mock(), + mock(), + mock(), + mock(), + userRepository, + mock(), + mock(), + mock(), + mock(), + mock(), + projectService, + eventService, + ); + + beforeEach(() => { + jest.restoreAllMocks(); + }); + + describe('changeGlobalRole', () => { + it('should emit event user-changed-role', async () => { + const request = mock({ + user: { id: '123' }, + params: { id: '456' }, + body: { newRoleName: 'global:member' }, + }); + userRepository.findOne.mockResolvedValue(mock({ id: '456' })); + projectService.getUserOwnedOrAdminProjects.mockResolvedValue([]); + + await controller.changeGlobalRole(request); + + expect(eventService.emit).toHaveBeenCalledWith('user-changed-role', { + userId: '123', + targetUserId: '456', + targetUserNewRole: 'global:member', + publicApi: false, + }); + }); + }); +}); diff --git a/packages/cli/src/controllers/communityPackages.controller.ts b/packages/cli/src/controllers/communityPackages.controller.ts index e6323c728cb40..d9e7f49719679 100644 --- a/packages/cli/src/controllers/communityPackages.controller.ts +++ b/packages/cli/src/controllers/communityPackages.controller.ts @@ -1,11 +1,9 @@ -import { Request, Response, NextFunction } from 'express'; -import config from '@/config'; import { RESPONSE_ERROR_MESSAGES, STARTER_TEMPLATE_NAME, UNKNOWN_FAILURE_REASON, } from '@/constants'; -import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } from '@/decorators'; +import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@/decorators'; import { NodeRequest } from '@/requests'; import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { CommunityPackages } from '@/Interfaces'; @@ -40,17 +38,6 @@ export class CommunityPackagesController { private readonly eventService: EventService, ) {} - // TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')` - @Middleware() - checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) { - if (config.getEnv('executions.mode') === 'queue' && req.method !== 'GET') - res.status(400).json({ - status: 'error', - message: 'Package management is disabled when running in "queue" mode', - }); - else next(); - } - @Post('/') @GlobalScope('communityPackage:install') async installPackage(req: NodeRequest.Post) { @@ -99,7 +86,7 @@ export class CommunityPackagesController { let installedPackage: InstalledPackages; try { - installedPackage = await this.communityPackagesService.installNpmModule( + installedPackage = await this.communityPackagesService.installPackage( parsed.packageName, parsed.version, ); @@ -207,7 +194,7 @@ export class CommunityPackagesController { } try { - await this.communityPackagesService.removeNpmModule(name, installedPackage); + await this.communityPackagesService.removePackage(name, installedPackage); } catch (error) { const message = [ `Error removing package "${name}"`, @@ -252,7 +239,7 @@ export class CommunityPackagesController { } try { - const newInstalledPackage = await this.communityPackagesService.updateNpmModule( + const newInstalledPackage = await this.communityPackagesService.updatePackage( this.communityPackagesService.parseNpmPackageName(name).packageName, previouslyInstalledPackage, ); diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index edf3b5c151fcc..19bd803c5e6c0 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -16,7 +16,6 @@ import type { User } from '@/databases/entities/User'; import { UserRepository } from '@db/repositories/user.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; -import { InternalHooks } from '@/InternalHooks'; import { ExternalHooks } from '@/ExternalHooks'; import { EventService } from '@/events/event.service'; @@ -24,7 +23,6 @@ import { EventService } from '@/events/event.service'; export class InvitationController { constructor( private readonly logger: Logger, - private readonly internalHooks: InternalHooks, private readonly externalHooks: ExternalHooks, private readonly authService: AuthService, private readonly userService: UserService, @@ -168,11 +166,11 @@ export class InvitationController { this.authService.issueCookie(res, updatedUser, req.browserId); - this.internalHooks.onUserSignup(updatedUser, { - user_type: 'email', - was_disabled_ldap_user: false, + this.eventService.emit('user-signed-up', { + user: updatedUser, + userType: 'email', + wasDisabledLdapUser: false, }); - this.eventService.emit('user-signed-up', { user: updatedUser }); const publicInvitee = await this.userService.toPublic(invitee); diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 39dfc93ab7406..2a0526b3cc8bb 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -19,7 +19,6 @@ import { isSamlLicensedAndEnabled } from '@/sso/saml/samlHelpers'; import { UserService } from '@/services/user.service'; import { Logger } from '@/Logger'; import { ExternalHooks } from '@/ExternalHooks'; -import { InternalHooks } from '@/InternalHooks'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { UserRepository } from '@/databases/repositories/user.repository'; import { isApiEnabled } from '@/PublicApi'; @@ -40,7 +39,6 @@ export class MeController { constructor( private readonly logger: Logger, private readonly externalHooks: ExternalHooks, - private readonly internalHooks: InternalHooks, private readonly authService: AuthService, private readonly userService: UserService, private readonly passwordUtility: PasswordUtility, @@ -91,6 +89,7 @@ export class MeController { await this.externalHooks.run('user.profile.beforeUpdate', [userId, currentEmail, payload]); + const preUpdateUser = await this.userRepository.findOneByOrFail({ id: userId }); await this.userService.update(userId, payload); const user = await this.userRepository.findOneOrFail({ where: { id: userId }, @@ -100,8 +99,10 @@ export class MeController { this.authService.issueCookie(res, user, req.browserId); - const fieldsChanged = Object.keys(payload); - this.internalHooks.onUserUpdate({ user, fields_changed: fieldsChanged }); + const fieldsChanged = (Object.keys(payload) as Array).filter( + (key) => payload[key] !== preUpdateUser[key], + ); + this.eventService.emit('user-updated', { user, fieldsChanged }); const publicUser = await this.userService.toPublic(user); @@ -151,7 +152,6 @@ export class MeController { this.authService.issueCookie(res, updatedUser, req.browserId); - this.internalHooks.onUserUpdate({ user: updatedUser, fields_changed: ['password'] }); this.eventService.emit('user-updated', { user: updatedUser, fieldsChanged: ['password'] }); await this.externalHooks.run('user.password.update', [updatedUser.email, updatedUser.password]); @@ -186,7 +186,10 @@ export class MeController { this.logger.info('User survey updated successfully', { userId: req.user.id }); - this.internalHooks.onPersonalizationSurveySubmitted(req.user.id, personalizationAnswers); + this.eventService.emit('user-submitted-personalization-survey', { + userId: req.user.id, + answers: personalizationAnswers, + }); return { success: true }; } diff --git a/packages/cli/test/unit/controllers/oauth/oAuth1Credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oAuth1Credential.controller.test.ts similarity index 99% rename from packages/cli/test/unit/controllers/oauth/oAuth1Credential.controller.test.ts rename to packages/cli/src/controllers/oauth/__tests__/oAuth1Credential.controller.test.ts index cbadc1cf87716..fae443b4ce9a2 100644 --- a/packages/cli/test/unit/controllers/oauth/oAuth1Credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oAuth1Credential.controller.test.ts @@ -19,7 +19,7 @@ import { CredentialsHelper } from '@/CredentialsHelper'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import { mockInstance } from '../../../shared/mocking'; +import { mockInstance } from '@test/mocking'; describe('OAuth1CredentialController', () => { mockInstance(Logger); diff --git a/packages/cli/test/unit/controllers/oauth/oAuth2Credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oAuth2Credential.controller.test.ts similarity index 99% rename from packages/cli/test/unit/controllers/oauth/oAuth2Credential.controller.test.ts rename to packages/cli/src/controllers/oauth/__tests__/oAuth2Credential.controller.test.ts index f2b718fda0090..f354a1ec024e7 100644 --- a/packages/cli/test/unit/controllers/oauth/oAuth2Credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oAuth2Credential.controller.test.ts @@ -19,7 +19,7 @@ import { CredentialsHelper } from '@/CredentialsHelper'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import { mockInstance } from '../../../shared/mocking'; +import { mockInstance } from '@test/mocking'; describe('OAuth2CredentialController', () => { mockInstance(Logger); diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 84d3b40124ad8..82506e952bc32 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -215,17 +215,16 @@ export class PasswordResetController { this.authService.issueCookie(res, user, req.browserId); - this.internalHooks.onUserUpdate({ user, fields_changed: ['password'] }); this.eventService.emit('user-updated', { user, fieldsChanged: ['password'] }); - // if this user used to be an LDAP users + // if this user used to be an LDAP user const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap'); if (ldapIdentity) { - this.internalHooks.onUserSignup(user, { - user_type: 'email', - was_disabled_ldap_user: true, + this.eventService.emit('user-signed-up', { + user, + userType: 'email', + wasDisabledLdapUser: true, }); - this.eventService.emit('user-signed-up', { user }); } await this.externalHooks.run('user.password.update', [user.email, passwordHash]); diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index aee2fd28c2a27..f3055ee4dc179 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -9,7 +9,7 @@ import { UserRoleChangePayload, UserSettingsUpdatePayload, } from '@/requests'; -import type { PublicUser, ITelemetryUserDeletionData } from '@/Interfaces'; +import type { PublicUser } from '@/Interfaces'; import { AuthIdentity } from '@db/entities/AuthIdentity'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; @@ -21,7 +21,6 @@ import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ExternalHooks } from '@/ExternalHooks'; -import { InternalHooks } from '@/InternalHooks'; import { validateEntity } from '@/GenericHelpers'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { Project } from '@/databases/entities/Project'; @@ -35,7 +34,6 @@ export class UsersController { constructor( private readonly logger: Logger, private readonly externalHooks: ExternalHooks, - private readonly internalHooks: InternalHooks, private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly userRepository: UserRepository, @@ -183,12 +181,7 @@ export class UsersController { ); } - const telemetryData: ITelemetryUserDeletionData = { - user_id: req.user.id, - target_user_old_status: userToDelete.isPending ? 'invited' : 'active', - target_user_id: idToDelete, - migration_strategy: transferId ? 'transfer_data' : 'delete_data', - }; + let transfereeId; if (transferId) { const transfereePersonalProject = await this.projectRepository.findOneBy({ id: transferId }); @@ -206,7 +199,7 @@ export class UsersController { }, }); - telemetryData.migration_user_id = transferee.id; + transfereeId = transferee.id; await this.userService.getManager().transaction(async (trx) => { await this.workflowService.transferAll( @@ -253,12 +246,14 @@ export class UsersController { await trx.delete(User, { id: userToDelete.id }); }); - this.internalHooks.onUserDeletion({ + this.eventService.emit('user-deleted', { user: req.user, - telemetryData, publicApi: false, + targetUserOldStatus: userToDelete.isPending ? 'invited' : 'active', + targetUserId: idToDelete, + migrationStrategy: transferId ? 'transfer_data' : 'delete_data', + migrationUserId: transfereeId, }); - this.eventService.emit('user-deleted', { user: req.user }); await this.externalHooks.run('user.deleted', [await this.userService.toPublic(userToDelete)]); @@ -294,11 +289,11 @@ export class UsersController { await this.userService.update(targetUser.id, { role: payload.newRoleName }); - this.internalHooks.onUserRoleChange({ - user: req.user, - target_user_id: targetUser.id, - target_user_new_role: ['global', payload.newRoleName].join(' '), - public_api: false, + this.eventService.emit('user-changed-role', { + userId: req.user.id, + targetUserId: targetUser.id, + targetUserNewRole: payload.newRoleName, + publicApi: false, }); const projects = await this.projectService.getUserOwnedOrAdminProjects(targetUser.id); diff --git a/packages/cli/src/credentials/__tests__/credentials.service.test.ts b/packages/cli/src/credentials/__tests__/credentials.service.test.ts index 18216df8674ce..a5fa3ca183cb7 100644 --- a/packages/cli/src/credentials/__tests__/credentials.service.test.ts +++ b/packages/cli/src/credentials/__tests__/credentials.service.test.ts @@ -3,7 +3,7 @@ import { mock } from 'jest-mock-extended'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialTypes } from '@/CredentialTypes'; -import { CredentialsService } from '../credentials.service'; +import { CredentialsService } from '@/credentials/credentials.service'; describe('CredentialsService', () => { const credType = mock({ diff --git a/packages/cli/test/unit/databases/entities/user.entity.test.ts b/packages/cli/src/databases/entities/__tests__/user.entity.test.ts similarity index 100% rename from packages/cli/test/unit/databases/entities/user.entity.test.ts rename to packages/cli/src/databases/entities/__tests__/user.entity.test.ts diff --git a/packages/cli/test/unit/repositories/execution.repository.test.ts b/packages/cli/src/databases/repositories/__tests__/execution.repository.test.ts similarity index 95% rename from packages/cli/test/unit/repositories/execution.repository.test.ts rename to packages/cli/src/databases/repositories/__tests__/execution.repository.test.ts index f8aba2e8a1228..1a4929f5cdf6e 100644 --- a/packages/cli/test/unit/repositories/execution.repository.test.ts +++ b/packages/cli/src/databases/repositories/__tests__/execution.repository.test.ts @@ -8,8 +8,7 @@ import { mock } from 'jest-mock-extended'; import { ExecutionEntity } from '@db/entities/ExecutionEntity'; import { ExecutionRepository } from '@db/repositories/execution.repository'; -import { mockEntityManager } from '../../shared/mocking'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance, mockEntityManager } from '@test/mocking'; describe('ExecutionRepository', () => { const entityManager = mockEntityManager(ExecutionEntity); diff --git a/packages/cli/test/unit/repositories/sharedCredentials.repository.test.ts b/packages/cli/src/databases/repositories/__tests__/sharedCredentials.repository.test.ts similarity index 98% rename from packages/cli/test/unit/repositories/sharedCredentials.repository.test.ts rename to packages/cli/src/databases/repositories/__tests__/sharedCredentials.repository.test.ts index 8eb8b498145ef..6fb4bad4eacf3 100644 --- a/packages/cli/test/unit/repositories/sharedCredentials.repository.test.ts +++ b/packages/cli/src/databases/repositories/__tests__/sharedCredentials.repository.test.ts @@ -8,7 +8,7 @@ import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { GLOBAL_MEMBER_SCOPES, GLOBAL_OWNER_SCOPES } from '@/permissions/global-roles'; -import { mockEntityManager } from '../../shared/mocking'; +import { mockEntityManager } from '@test/mocking'; describe('SharedCredentialsRepository', () => { const entityManager = mockEntityManager(SharedCredentials); diff --git a/packages/cli/test/unit/repositories/workflowStatistics.test.ts b/packages/cli/src/databases/repositories/__tests__/workflowStatistics.test.ts similarity index 96% rename from packages/cli/test/unit/repositories/workflowStatistics.test.ts rename to packages/cli/src/databases/repositories/__tests__/workflowStatistics.test.ts index 86e0ee1c92bd1..7bed056549751 100644 --- a/packages/cli/test/unit/repositories/workflowStatistics.test.ts +++ b/packages/cli/src/databases/repositories/__tests__/workflowStatistics.test.ts @@ -5,7 +5,7 @@ import { mock, mockClear } from 'jest-mock-extended'; import { StatisticsNames, WorkflowStatistics } from '@db/entities/WorkflowStatistics'; import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository'; -import { mockEntityManager } from '../../shared/mocking'; +import { mockEntityManager } from '@test/mocking'; describe('insertWorkflowStatistics', () => { const entityManager = mockEntityManager(WorkflowStatistics); diff --git a/packages/cli/test/unit/databases/utils/customValidators.test.ts b/packages/cli/src/databases/utils/__tests__/customValidators.test.ts similarity index 100% rename from packages/cli/test/unit/databases/utils/customValidators.test.ts rename to packages/cli/src/databases/utils/__tests__/customValidators.test.ts diff --git a/packages/cli/test/unit/databases/utils/migrationHelpers.test.ts b/packages/cli/src/databases/utils/__tests__/migrationHelpers.test.ts similarity index 100% rename from packages/cli/test/unit/databases/utils/migrationHelpers.test.ts rename to packages/cli/src/databases/utils/__tests__/migrationHelpers.test.ts diff --git a/packages/cli/test/unit/decorators/OnShutdown.test.ts b/packages/cli/src/decorators/__tests__/OnShutdown.test.ts similarity index 100% rename from packages/cli/test/unit/decorators/OnShutdown.test.ts rename to packages/cli/src/decorators/__tests__/OnShutdown.test.ts diff --git a/packages/cli/test/unit/decorators/controller.registry.test.ts b/packages/cli/src/decorators/__tests__/controller.registry.test.ts similarity index 100% rename from packages/cli/test/unit/decorators/controller.registry.test.ts rename to packages/cli/src/decorators/__tests__/controller.registry.test.ts diff --git a/packages/cli/test/unit/GitService.test.ts b/packages/cli/src/environments/sourceControl/__tests__/sourceControlGit.service.test.ts similarity index 100% rename from packages/cli/test/unit/GitService.test.ts rename to packages/cli/src/environments/sourceControl/__tests__/sourceControlGit.service.test.ts diff --git a/packages/cli/test/unit/SourceControl.test.ts b/packages/cli/src/environments/sourceControl/__tests__/sourceControlHelper.ee.test.ts similarity index 99% rename from packages/cli/test/unit/SourceControl.test.ts rename to packages/cli/src/environments/sourceControl/__tests__/sourceControlHelper.ee.test.ts index 5eeccbb1b97f4..5141d36f2f7dd 100644 --- a/packages/cli/test/unit/SourceControl.test.ts +++ b/packages/cli/src/environments/sourceControl/__tests__/sourceControlHelper.ee.test.ts @@ -18,7 +18,7 @@ import { import { constants as fsConstants, accessSync } from 'fs'; import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile'; import type { SourceControlPreferences } from '@/environments/sourceControl/types/sourceControlPreferences'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; const pushResult: SourceControlledFile[] = [ { diff --git a/packages/cli/test/unit/utils.test.ts b/packages/cli/src/errors/response-errors/__tests__/webhook-not-found.error.test.ts similarity index 100% rename from packages/cli/test/unit/utils.test.ts rename to packages/cli/src/errors/response-errors/__tests__/webhook-not-found.error.test.ts diff --git a/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts b/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts index 4084bebeb228f..ebd56ee514c7e 100644 --- a/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts +++ b/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts @@ -242,6 +242,11 @@ describe('LogStreamingEventRelay', () => { lastName: 'Doe', role: 'some-role', }, + targetUserOldStatus: 'active', + publicApi: false, + migrationStrategy: 'transfer_data', + targetUserId: '456', + migrationUserId: '789', }; eventService.emit('user-deleted', event); diff --git a/packages/cli/src/events/relay-event-map.ts b/packages/cli/src/events/relay-event-map.ts index 193e85c9a4ed8..0d329508b413e 100644 --- a/packages/cli/src/events/relay-event-map.ts +++ b/packages/cli/src/events/relay-event-map.ts @@ -2,6 +2,7 @@ import type { AuthenticationMethod, IRun, IWorkflowBase } from 'n8n-workflow'; import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces'; import type { ProjectRole } from '@/databases/entities/ProjectRelation'; import type { GlobalRole } from '@/databases/entities/User'; +import type { AuthProviderType } from '@/databases/entities/AuthIdentity'; export type UserLike = { id: string; @@ -72,13 +73,26 @@ export type RelayEventMap = { // #region User + 'user-submitted-personalization-survey': { + userId: string; + answers: Record; + }; + 'user-deleted': { user: UserLike; + publicApi: boolean; + targetUserOldStatus: 'active' | 'invited'; + migrationStrategy?: 'transfer_data' | 'delete_data'; + targetUserId?: string; + migrationUserId?: string; }; 'user-invited': { user: UserLike; targetUserId: string[]; + publicApi: boolean; + emailSent: boolean; + inviteeRole: string; }; 'user-reinvited': { @@ -93,6 +107,8 @@ export type RelayEventMap = { 'user-signed-up': { user: UserLike; + userType: AuthProviderType; + wasDisabledLdapUser: boolean; }; 'user-logged-in': { @@ -106,6 +122,43 @@ export type RelayEventMap = { reason?: string; }; + 'user-changed-role': { + userId: string; + targetUserId: string; + publicApi: boolean; + targetUserNewRole: string; + }; + + 'user-retrieved-user': { + userId: string; + publicApi: boolean; + }; + + 'user-retrieved-all-users': { + userId: string; + publicApi: boolean; + }; + + 'user-retrieved-execution': { + userId: string; + publicApi: boolean; + }; + + 'user-retrieved-all-executions': { + userId: string; + publicApi: boolean; + }; + + 'user-retrieved-workflow': { + userId: string; + publicApi: boolean; + }; + + 'user-retrieved-all-workflows': { + userId: string; + publicApi: boolean; + }; + // #endregion // #region Click diff --git a/packages/cli/src/events/telemetry-event-relay.ts b/packages/cli/src/events/telemetry-event-relay.ts index 91f82a0c127a5..bc7a19479e721 100644 --- a/packages/cli/src/events/telemetry-event-relay.ts +++ b/packages/cli/src/events/telemetry-event-relay.ts @@ -17,6 +17,7 @@ import { ProjectRelationRepository } from '@/databases/repositories/projectRelat import type { IExecutionTrackProperties } from '@/Interfaces'; import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions'; import { EventRelay } from './event-relay'; +import { snakeCase } from 'change-case'; @Service() export class TelemetryEventRelay extends EventRelay { @@ -73,6 +74,19 @@ export class TelemetryEventRelay extends EventRelay { 'workflow-saved': async (event) => await this.workflowSaved(event), 'server-started': async () => await this.serverStarted(), 'workflow-post-execute': async (event) => await this.workflowPostExecute(event), + 'user-changed-role': (event) => this.userChangedRole(event), + 'user-retrieved-user': (event) => this.userRetrievedUser(event), + 'user-retrieved-all-users': (event) => this.userRetrievedAllUsers(event), + 'user-retrieved-execution': (event) => this.userRetrievedExecution(event), + 'user-retrieved-all-executions': (event) => this.userRetrievedAllExecutions(event), + 'user-retrieved-workflow': (event) => this.userRetrievedWorkflow(event), + 'user-retrieved-all-workflows': (event) => this.userRetrievedAllWorkflows(event), + 'user-updated': (event) => this.userUpdated(event), + 'user-deleted': (event) => this.userDeleted(event), + 'user-invited': (event) => this.userInvited(event), + 'user-signed-up': (event) => this.userSignedUp(event), + 'user-submitted-personalization-survey': (event) => + this.userSubmittedPersonalizationSurvey(event), }); } @@ -160,10 +174,6 @@ export class TelemetryEventRelay extends EventRelay { workflowUpdates, forced, }: RelayEventMap['source-control-user-pulled-api']) { - console.log('source-control-user-pulled-api', { - workflow_updates: workflowUpdates, - forced, - }); this.telemetry.track('User pulled via API', { workflow_updates: workflowUpdates, forced, @@ -744,4 +754,132 @@ export class TelemetryEventRelay extends EventRelay { } // #endregion + + // #region User + + private userChangedRole({ + userId, + targetUserId, + targetUserNewRole, + publicApi, + }: RelayEventMap['user-changed-role']) { + this.telemetry.track('User changed role', { + user_id: userId, + target_user_id: targetUserId, + target_user_new_role: targetUserNewRole, + public_api: publicApi, + }); + } + + private userRetrievedUser({ userId, publicApi }: RelayEventMap['user-retrieved-user']) { + this.telemetry.track('User retrieved user', { + user_id: userId, + public_api: publicApi, + }); + } + + private userRetrievedAllUsers({ userId, publicApi }: RelayEventMap['user-retrieved-all-users']) { + this.telemetry.track('User retrieved all users', { + user_id: userId, + public_api: publicApi, + }); + } + + private userRetrievedExecution({ userId, publicApi }: RelayEventMap['user-retrieved-execution']) { + this.telemetry.track('User retrieved execution', { + user_id: userId, + public_api: publicApi, + }); + } + + private userRetrievedAllExecutions({ + userId, + publicApi, + }: RelayEventMap['user-retrieved-all-executions']) { + this.telemetry.track('User retrieved all executions', { + user_id: userId, + public_api: publicApi, + }); + } + + private userRetrievedWorkflow({ userId, publicApi }: RelayEventMap['user-retrieved-workflow']) { + this.telemetry.track('User retrieved workflow', { + user_id: userId, + public_api: publicApi, + }); + } + + private userRetrievedAllWorkflows({ + userId, + publicApi, + }: RelayEventMap['user-retrieved-all-workflows']) { + this.telemetry.track('User retrieved all workflows', { + user_id: userId, + public_api: publicApi, + }); + } + + private userUpdated({ user, fieldsChanged }: RelayEventMap['user-updated']) { + this.telemetry.track('User changed personal settings', { + user_id: user.id, + fields_changed: fieldsChanged, + }); + } + + private userDeleted({ + user, + publicApi, + targetUserOldStatus, + migrationStrategy, + targetUserId, + migrationUserId, + }: RelayEventMap['user-deleted']) { + this.telemetry.track('User deleted user', { + user_id: user.id, + public_api: publicApi, + target_user_old_status: targetUserOldStatus, + migration_strategy: migrationStrategy, + target_user_id: targetUserId, + migration_user_id: migrationUserId, + }); + } + + private userInvited({ + user, + targetUserId, + publicApi, + emailSent, + inviteeRole, + }: RelayEventMap['user-invited']) { + this.telemetry.track('User invited new user', { + user_id: user.id, + target_user_id: targetUserId, + public_api: publicApi, + email_sent: emailSent, + invitee_role: inviteeRole, + }); + } + + private userSignedUp({ user, userType, wasDisabledLdapUser }: RelayEventMap['user-signed-up']) { + this.telemetry.track('User signed up', { + user_id: user.id, + user_type: userType, + was_disabled_ldap_user: wasDisabledLdapUser, + }); + } + + private userSubmittedPersonalizationSurvey({ + userId, + answers, + }: RelayEventMap['user-submitted-personalization-survey']) { + const camelCaseKeys = Object.keys(answers); + const personalizationSurveyData = { user_id: userId } as Record; + camelCaseKeys.forEach((camelCaseKey) => { + personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey]; + }); + + this.telemetry.track('User responded to personalization questions', personalizationSurveyData); + } + + // #endregion } diff --git a/packages/cli/test/unit/execution-lifecyle/restoreBinaryDataId.test.ts b/packages/cli/src/executionLifecycleHooks/__tests__/restoreBinaryDataId.test.ts similarity index 97% rename from packages/cli/test/unit/execution-lifecyle/restoreBinaryDataId.test.ts rename to packages/cli/src/executionLifecycleHooks/__tests__/restoreBinaryDataId.test.ts index 3fcdb79c72c7e..ea962882bd782 100644 --- a/packages/cli/test/unit/execution-lifecyle/restoreBinaryDataId.test.ts +++ b/packages/cli/src/executionLifecycleHooks/__tests__/restoreBinaryDataId.test.ts @@ -1,6 +1,6 @@ import { restoreBinaryDataId } from '@/executionLifecycleHooks/restoreBinaryDataId'; import { BinaryDataService } from 'n8n-core'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import type { IRun } from 'n8n-workflow'; import config from '@/config'; @@ -24,7 +24,7 @@ function toIRun(item?: object) { } function getDataId(run: IRun, kind: 'binary' | 'json') { - // @ts-ignore + // @ts-expect-error The type doesn't have the correct structure return run.data.resultData.runData.myNode[0].data.main[0][0][kind].data.id; } diff --git a/packages/cli/test/unit/execution-lifecyle/saveExecutionProgress.test.ts b/packages/cli/src/executionLifecycleHooks/__tests__/saveExecutionProgress.test.ts similarity index 98% rename from packages/cli/test/unit/execution-lifecyle/saveExecutionProgress.test.ts rename to packages/cli/src/executionLifecycleHooks/__tests__/saveExecutionProgress.test.ts index 4235e56ecbad3..9b1faa7f60540 100644 --- a/packages/cli/test/unit/execution-lifecyle/saveExecutionProgress.test.ts +++ b/packages/cli/src/executionLifecycleHooks/__tests__/saveExecutionProgress.test.ts @@ -1,5 +1,5 @@ import { ExecutionRepository } from '@/databases/repositories/execution.repository'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { Logger } from '@/Logger'; import { saveExecutionProgress } from '@/executionLifecycleHooks/saveExecutionProgress'; import * as fnModule from '@/executionLifecycleHooks/toSaveSettings'; diff --git a/packages/cli/test/unit/execution-lifecyle/toSaveSettings.test.ts b/packages/cli/src/executionLifecycleHooks/__tests__/toSaveSettings.test.ts similarity index 98% rename from packages/cli/test/unit/execution-lifecyle/toSaveSettings.test.ts rename to packages/cli/src/executionLifecycleHooks/__tests__/toSaveSettings.test.ts index 6fc516a0ea214..57379e0e73055 100644 --- a/packages/cli/test/unit/execution-lifecyle/toSaveSettings.test.ts +++ b/packages/cli/src/executionLifecycleHooks/__tests__/toSaveSettings.test.ts @@ -35,7 +35,7 @@ describe('failed production executions', () => { }); }); -describe('sucessful production executions', () => { +describe('successful production executions', () => { it('should favor workflow settings over defaults', () => { config.set('executions.saveDataOnSuccess', 'none'); diff --git a/packages/cli/test/unit/controllers/executions.controller.test.ts b/packages/cli/src/executions/__tests__/executions.controller.test.ts similarity index 100% rename from packages/cli/test/unit/controllers/executions.controller.test.ts rename to packages/cli/src/executions/__tests__/executions.controller.test.ts diff --git a/packages/cli/test/unit/middleware/executions/parse-range-query.middleware.test.ts b/packages/cli/src/executions/__tests__/parse-range-query.middleware.test.ts similarity index 100% rename from packages/cli/test/unit/middleware/executions/parse-range-query.middleware.test.ts rename to packages/cli/src/executions/__tests__/parse-range-query.middleware.test.ts diff --git a/packages/cli/test/unit/license/license.service.test.ts b/packages/cli/src/license/__tests__/license.service.test.ts similarity index 100% rename from packages/cli/test/unit/license/license.service.test.ts rename to packages/cli/src/license/__tests__/license.service.test.ts diff --git a/packages/cli/test/unit/middleware/listQuery.test.ts b/packages/cli/src/middlewares/listQuery/__tests__/listQuery.test.ts similarity index 100% rename from packages/cli/test/unit/middleware/listQuery.test.ts rename to packages/cli/src/middlewares/listQuery/__tests__/listQuery.test.ts diff --git a/packages/cli/test/unit/PostHog.test.ts b/packages/cli/src/posthog/__tests__/PostHog.test.ts similarity index 97% rename from packages/cli/test/unit/PostHog.test.ts rename to packages/cli/src/posthog/__tests__/PostHog.test.ts index 5798c0cce2217..f1604f7253df1 100644 --- a/packages/cli/test/unit/PostHog.test.ts +++ b/packages/cli/src/posthog/__tests__/PostHog.test.ts @@ -2,7 +2,7 @@ import { PostHog } from 'posthog-node'; import { InstanceSettings } from 'n8n-core'; import { PostHogClient } from '@/posthog'; import config from '@/config'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; jest.mock('posthog-node'); diff --git a/packages/cli/test/unit/push/index.test.ts b/packages/cli/src/push/__tests__/index.test.ts similarity index 96% rename from packages/cli/test/unit/push/index.test.ts rename to packages/cli/src/push/__tests__/index.test.ts index 1736693509374..a61496b0c9248 100644 --- a/packages/cli/test/unit/push/index.test.ts +++ b/packages/cli/src/push/__tests__/index.test.ts @@ -9,7 +9,7 @@ import { WebSocketPush } from '@/push/websocket.push'; import type { WebSocketPushRequest, SSEPushRequest } from '@/push/types'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; jest.unmock('@/push'); diff --git a/packages/cli/test/unit/push/websocket.push.test.ts b/packages/cli/src/push/__tests__/websocket.push.test.ts similarity index 98% rename from packages/cli/test/unit/push/websocket.push.test.ts rename to packages/cli/src/push/__tests__/websocket.push.test.ts index 7531e43776c2c..f1a0e577f9ad8 100644 --- a/packages/cli/test/unit/push/websocket.push.test.ts +++ b/packages/cli/src/push/__tests__/websocket.push.test.ts @@ -6,7 +6,7 @@ import { WebSocketPush } from '@/push/websocket.push'; import { Logger } from '@/Logger'; import type { PushDataExecutionRecovered } from '@/Interfaces'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; jest.useFakeTimers(); diff --git a/packages/cli/test/unit/ExecutionMetadataService.test.ts b/packages/cli/src/services/__tests__/ExecutionMetadataService.test.ts similarity index 94% rename from packages/cli/test/unit/ExecutionMetadataService.test.ts rename to packages/cli/src/services/__tests__/ExecutionMetadataService.test.ts index 826aae5e25324..8b77b8b1681d4 100644 --- a/packages/cli/test/unit/ExecutionMetadataService.test.ts +++ b/packages/cli/src/services/__tests__/ExecutionMetadataService.test.ts @@ -1,7 +1,7 @@ import { Container } from 'typedi'; import { ExecutionMetadataRepository } from '@db/repositories/executionMetadata.repository'; import { ExecutionMetadataService } from '@/services/executionMetadata.service'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; describe('ExecutionMetadataService', () => { const repository = mockInstance(ExecutionMetadataRepository); diff --git a/packages/cli/test/unit/services/activeWorkflows.service.test.ts b/packages/cli/src/services/__tests__/activeWorkflows.service.test.ts similarity index 100% rename from packages/cli/test/unit/services/activeWorkflows.service.test.ts rename to packages/cli/src/services/__tests__/activeWorkflows.service.test.ts diff --git a/packages/cli/test/unit/services/communityPackages.service.test.ts b/packages/cli/src/services/__tests__/communityPackages.service.test.ts similarity index 89% rename from packages/cli/test/unit/services/communityPackages.service.test.ts rename to packages/cli/src/services/__tests__/communityPackages.service.test.ts index 2b0d8bf0d452a..8d3289a8695b2 100644 --- a/packages/cli/test/unit/services/communityPackages.service.test.ts +++ b/packages/cli/src/services/__tests__/communityPackages.service.test.ts @@ -2,8 +2,10 @@ import { exec } from 'child_process'; import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; import axios from 'axios'; import { mocked } from 'jest-mock'; -import Container from 'typedi'; +import { mock } from 'jest-mock-extended'; +import type { GlobalConfig } from '@n8n/config'; import type { PublicInstalledPackage } from 'n8n-workflow'; +import type { PackageDirectoryLoader } from 'n8n-core'; import { NODE_PACKAGE_PREFIX, @@ -11,24 +13,18 @@ import { NPM_PACKAGE_STATUS_GOOD, RESPONSE_ERROR_MESSAGES, } from '@/constants'; -import config from '@/config'; import { InstalledPackages } from '@db/entities/InstalledPackages'; import type { CommunityPackages } from '@/Interfaces'; import { CommunityPackagesService } from '@/services/communityPackages.service'; import { InstalledNodesRepository } from '@db/repositories/installedNodes.repository'; import { InstalledPackagesRepository } from '@db/repositories/installedPackages.repository'; import { InstalledNodes } from '@db/entities/InstalledNodes'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import type { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { mockInstance } from '../../shared/mocking'; -import { - COMMUNITY_NODE_VERSION, - COMMUNITY_PACKAGE_VERSION, -} from '../../integration/shared/constants'; -import { randomName } from '../../integration/shared/random'; -import { mockPackageName, mockPackagePair } from '../../integration/shared/utils'; -import { InstanceSettings, PackageDirectoryLoader } from 'n8n-core'; -import { Logger } from '@/Logger'; +import { mockInstance } from '@test/mocking'; +import { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION } from '@test-integration/constants'; +import { randomName } from '@test-integration/random'; +import { mockPackageName, mockPackagePair } from '@test-integration/utils'; jest.mock('fs/promises'); jest.mock('child_process'); @@ -43,6 +39,15 @@ const execMock = ((...args) => { }) as typeof exec; describe('CommunityPackagesService', () => { + const globalConfig = mock({ + nodes: { + communityPackages: { + reinstallMissing: false, + }, + }, + }); + const loadNodesAndCredentials = mock(); + const installedNodesRepository = mockInstance(InstalledNodesRepository); installedNodesRepository.create.mockImplementation(() => { const nodeName = randomName(); @@ -63,13 +68,14 @@ describe('CommunityPackagesService', () => { }); }); - mockInstance(LoadNodesAndCredentials); - - const communityPackagesService = Container.get(CommunityPackagesService); - - beforeEach(() => { - config.load(config.default); - }); + const communityPackagesService = new CommunityPackagesService( + mock(), + mock(), + mock(), + loadNodesAndCredentials, + mock(), + globalConfig, + ); describe('parseNpmPackageName()', () => { test('should fail with empty package name', () => { @@ -368,29 +374,12 @@ describe('CommunityPackagesService', () => { }; describe('updateNpmModule', () => { - let packageDirectoryLoader: PackageDirectoryLoader; - let communityPackagesService: CommunityPackagesService; + const packageDirectoryLoader = mock(); beforeEach(async () => { - jest.restoreAllMocks(); + jest.clearAllMocks(); - packageDirectoryLoader = mockInstance(PackageDirectoryLoader); - const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials); loadNodesAndCredentials.loadPackage.mockResolvedValue(packageDirectoryLoader); - const instanceSettings = mockInstance(InstanceSettings); - const logger = mockInstance(Logger); - const installedPackagesRepository = mockInstance(InstalledPackagesRepository); - - communityPackagesService = new CommunityPackagesService( - instanceSettings, - logger, - installedPackagesRepository, - loadNodesAndCredentials, - ); - }); - - afterEach(async () => { - jest.restoreAllMocks(); }); test('should call `exec` with the correct command ', async () => { @@ -408,10 +397,7 @@ describe('CommunityPackagesService', () => { // // ACT // - await communityPackagesService.updateNpmModule( - installedPackage.packageName, - installedPackage, - ); + await communityPackagesService.updatePackage(installedPackage.packageName, installedPackage); // // ASSERT diff --git a/packages/cli/test/unit/credentials-tester.unit.test.ts b/packages/cli/src/services/__tests__/credentials-tester.service.test.ts similarity index 100% rename from packages/cli/test/unit/credentials-tester.unit.test.ts rename to packages/cli/src/services/__tests__/credentials-tester.service.test.ts diff --git a/packages/cli/test/unit/services/curl.service.test.ts b/packages/cli/src/services/__tests__/curl.service.test.ts similarity index 100% rename from packages/cli/test/unit/services/curl.service.test.ts rename to packages/cli/src/services/__tests__/curl.service.test.ts diff --git a/packages/cli/test/unit/services/hooks.service.test.ts b/packages/cli/src/services/__tests__/hooks.service.test.ts similarity index 100% rename from packages/cli/test/unit/services/hooks.service.test.ts rename to packages/cli/src/services/__tests__/hooks.service.test.ts diff --git a/packages/cli/test/unit/services/jwt.service.test.ts b/packages/cli/src/services/__tests__/jwt.service.test.ts similarity index 100% rename from packages/cli/test/unit/services/jwt.service.test.ts rename to packages/cli/src/services/__tests__/jwt.service.test.ts diff --git a/packages/cli/test/unit/services/naming.service.test.ts b/packages/cli/src/services/__tests__/naming.service.test.ts similarity index 98% rename from packages/cli/test/unit/services/naming.service.test.ts rename to packages/cli/src/services/__tests__/naming.service.test.ts index ea2c34fb8c1ee..1ca216734630e 100644 --- a/packages/cli/test/unit/services/naming.service.test.ts +++ b/packages/cli/src/services/__tests__/naming.service.test.ts @@ -1,6 +1,6 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { NamingService } from '@/services/naming.service'; import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import type { CredentialsEntity } from '@/databases/entities/CredentialsEntity'; diff --git a/packages/cli/test/unit/services/orchestration.service.test.ts b/packages/cli/src/services/__tests__/orchestration.service.test.ts similarity index 99% rename from packages/cli/test/unit/services/orchestration.service.test.ts rename to packages/cli/src/services/__tests__/orchestration.service.test.ts index 0b63f74344b57..c69e674613e25 100644 --- a/packages/cli/test/unit/services/orchestration.service.test.ts +++ b/packages/cli/src/services/__tests__/orchestration.service.test.ts @@ -17,7 +17,7 @@ import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager import { Logger } from '@/Logger'; import { Push } from '@/push'; import { ActiveWorkflowManager } from '@/ActiveWorkflowManager'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { RedisClientService } from '@/services/redis/redis-client.service'; const instanceSettings = Container.get(InstanceSettings); diff --git a/packages/cli/test/unit/services/ownership.service.test.ts b/packages/cli/src/services/__tests__/ownership.service.test.ts similarity index 98% rename from packages/cli/test/unit/services/ownership.service.test.ts rename to packages/cli/src/services/__tests__/ownership.service.test.ts index d1a722da19625..8a3d40eb60f8d 100644 --- a/packages/cli/test/unit/services/ownership.service.test.ts +++ b/packages/cli/src/services/__tests__/ownership.service.test.ts @@ -3,14 +3,14 @@ import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.reposi import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { User } from '@db/entities/User'; import type { SharedCredentials } from '@db/entities/SharedCredentials'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import { UserRepository } from '@/databases/repositories/user.repository'; import { mock } from 'jest-mock-extended'; import { Project } from '@/databases/entities/Project'; import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; import { ProjectRelation } from '@/databases/entities/ProjectRelation'; -import { mockCredential, mockProject } from '../shared/mockObjects'; +import { mockCredential, mockProject } from '@test/mockObjects'; describe('OwnershipService', () => { const userRepository = mockInstance(UserRepository); diff --git a/packages/cli/test/unit/utilities/password.utility.test.ts b/packages/cli/src/services/__tests__/password.utility.test.ts similarity index 100% rename from packages/cli/test/unit/utilities/password.utility.test.ts rename to packages/cli/src/services/__tests__/password.utility.test.ts diff --git a/packages/cli/test/unit/services/redis.service.test.ts b/packages/cli/src/services/__tests__/redis.service.test.ts similarity index 94% rename from packages/cli/test/unit/services/redis.service.test.ts rename to packages/cli/src/services/__tests__/redis.service.test.ts index cb963ad535567..1d9652983735c 100644 --- a/packages/cli/test/unit/services/redis.service.test.ts +++ b/packages/cli/src/services/__tests__/redis.service.test.ts @@ -2,7 +2,7 @@ import Container from 'typedi'; import { Logger } from '@/Logger'; import config from '@/config'; import { RedisService } from '@/services/redis.service'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; jest.mock('ioredis', () => { const Redis = require('ioredis-mock'); @@ -14,7 +14,7 @@ jest.mock('ioredis', () => { }; } // second mock for our code - return function (...args: any) { + return function (...args: unknown[]) { return new Redis(args); }; }); diff --git a/packages/cli/test/unit/services/test-webhook-registrations.service.test.ts b/packages/cli/src/services/__tests__/test-webhook-registrations.service.test.ts similarity index 100% rename from packages/cli/test/unit/services/test-webhook-registrations.service.test.ts rename to packages/cli/src/services/__tests__/test-webhook-registrations.service.test.ts diff --git a/packages/cli/test/unit/services/user.service.test.ts b/packages/cli/src/services/__tests__/user.service.test.ts similarity index 98% rename from packages/cli/test/unit/services/user.service.test.ts rename to packages/cli/src/services/__tests__/user.service.test.ts index 89929f57b0b89..5aeb919220c23 100644 --- a/packages/cli/test/unit/services/user.service.test.ts +++ b/packages/cli/src/services/__tests__/user.service.test.ts @@ -4,7 +4,7 @@ import { v4 as uuid } from 'uuid'; import { User } from '@db/entities/User'; import { UserService } from '@/services/user.service'; import { UrlService } from '@/services/url.service'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { UserRepository } from '@/databases/repositories/user.repository'; import { GlobalConfig } from '@n8n/config'; diff --git a/packages/cli/test/unit/services/webhook.service.test.ts b/packages/cli/src/services/__tests__/webhook.service.test.ts similarity index 99% rename from packages/cli/test/unit/services/webhook.service.test.ts rename to packages/cli/src/services/__tests__/webhook.service.test.ts index adb18de4b309b..181bc60752956 100644 --- a/packages/cli/test/unit/services/webhook.service.test.ts +++ b/packages/cli/src/services/__tests__/webhook.service.test.ts @@ -4,7 +4,7 @@ import { WebhookRepository } from '@db/repositories/webhook.repository'; import { CacheService } from '@/services/cache/cache.service'; import { WebhookService } from '@/services/webhook.service'; import { WebhookEntity } from '@db/entities/WebhookEntity'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; const createWebhook = (method: string, path: string, webhookId?: string, pathSegments?: number) => Object.assign(new WebhookEntity(), { diff --git a/packages/cli/test/unit/services/workflow-statistics.service.test.ts b/packages/cli/src/services/__tests__/workflow-statistics.service.test.ts similarity index 99% rename from packages/cli/test/unit/services/workflow-statistics.service.test.ts rename to packages/cli/src/services/__tests__/workflow-statistics.service.test.ts index 6d9baf49ea939..24c3bce560d32 100644 --- a/packages/cli/test/unit/services/workflow-statistics.service.test.ts +++ b/packages/cli/src/services/__tests__/workflow-statistics.service.test.ts @@ -17,7 +17,7 @@ import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistic import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; import { UserService } from '@/services/user.service'; import { OwnershipService } from '@/services/ownership.service'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import type { Project } from '@/databases/entities/Project'; describe('WorkflowStatisticsService', () => { diff --git a/packages/cli/test/unit/services/cache-mock.service.test.ts b/packages/cli/src/services/cache/__tests__/cache-mock.service.test.ts similarity index 100% rename from packages/cli/test/unit/services/cache-mock.service.test.ts rename to packages/cli/src/services/cache/__tests__/cache-mock.service.test.ts diff --git a/packages/cli/test/unit/services/cache.service.test.ts b/packages/cli/src/services/cache/__tests__/cache.service.test.ts similarity index 92% rename from packages/cli/test/unit/services/cache.service.test.ts rename to packages/cli/src/services/cache/__tests__/cache.service.test.ts index a742b20698d46..5c024b2903cc7 100644 --- a/packages/cli/test/unit/services/cache.service.test.ts +++ b/packages/cli/src/services/cache/__tests__/cache.service.test.ts @@ -1,6 +1,8 @@ import { CacheService } from '@/services/cache/cache.service'; import config from '@/config'; import { sleep } from 'n8n-workflow'; +import { GlobalConfig } from '@n8n/config'; +import Container from 'typedi'; jest.mock('ioredis', () => { const Redis = require('ioredis-mock'); @@ -13,10 +15,12 @@ jest.mock('ioredis', () => { for (const backend of ['memory', 'redis'] as const) { describe(backend, () => { let cacheService: CacheService; + let globalConfig: GlobalConfig; beforeAll(async () => { - config.set('cache.backend', backend); - cacheService = new CacheService(); + globalConfig = Container.get(GlobalConfig); + globalConfig.cache.backend = backend; + cacheService = new CacheService(globalConfig); await cacheService.init(); }); @@ -43,7 +47,7 @@ for (const backend of ['memory', 'redis'] as const) { if (backend === 'memory') { test('should honor max size when enough', async () => { - config.set('cache.memory.maxSize', 16); // enough bytes for "withoutUnicode" + globalConfig.cache.memory.maxSize = 16; // enough bytes for "withoutUnicode" await cacheService.init(); await cacheService.set('key', 'withoutUnicode'); @@ -51,12 +55,12 @@ for (const backend of ['memory', 'redis'] as const) { await expect(cacheService.get('key')).resolves.toBe('withoutUnicode'); // restore - config.set('cache.memory.maxSize', 3 * 1024 * 1024); + globalConfig.cache.memory.maxSize = 3 * 1024 * 1024; await cacheService.init(); }); test('should honor max size when not enough', async () => { - config.set('cache.memory.maxSize', 16); // not enough bytes for "withUnicodeԱԲԳ" + globalConfig.cache.memory.maxSize = 16; // not enough bytes for "withUnicodeԱԲԳ" await cacheService.init(); await cacheService.set('key', 'withUnicodeԱԲԳ'); @@ -64,7 +68,8 @@ for (const backend of ['memory', 'redis'] as const) { await expect(cacheService.get('key')).resolves.toBeUndefined(); // restore - config.set('cache.memory.maxSize', 3 * 1024 * 1024); + globalConfig.cache.memory.maxSize = 3 * 1024 * 1024; + // restore await cacheService.init(); }); } diff --git a/packages/cli/src/services/cache/cache.service.ts b/packages/cli/src/services/cache/cache.service.ts index 75dad03b49d26..daf51911ff362 100644 --- a/packages/cli/src/services/cache/cache.service.ts +++ b/packages/cli/src/services/cache/cache.service.ts @@ -13,6 +13,7 @@ import type { } from '@/services/cache/cache.types'; import { TIME } from '@/constants'; import { TypedEmitter } from '@/TypedEmitter'; +import { GlobalConfig } from '@n8n/config'; type CacheEvents = { 'metrics.cache.hit': never; @@ -22,12 +23,15 @@ type CacheEvents = { @Service() export class CacheService extends TypedEmitter { + constructor(private readonly globalConfig: GlobalConfig) { + super(); + } + private cache: TaggedRedisCache | TaggedMemoryCache; async init() { - const backend = config.getEnv('cache.backend'); + const { backend } = this.globalConfig.cache; const mode = config.getEnv('executions.mode'); - const ttl = config.getEnv('cache.redis.ttl'); const useRedis = backend === 'redis' || (backend === 'auto' && mode === 'queue'); @@ -36,8 +40,9 @@ export class CacheService extends TypedEmitter { const redisClientService = Container.get(RedisClientService); const prefixBase = config.getEnv('redis.prefix'); - const cachePrefix = config.getEnv('cache.redis.prefix'); - const prefix = redisClientService.toValidPrefix(`${prefixBase}:${cachePrefix}:`); + const prefix = redisClientService.toValidPrefix( + `${prefixBase}:${this.globalConfig.cache.redis.prefix}:`, + ); const redisClient = redisClientService.createClient({ type: 'client(cache)', @@ -45,7 +50,9 @@ export class CacheService extends TypedEmitter { }); const { redisStoreUsingClient } = await import('@/services/cache/redis.cache-manager'); - const redisStore = redisStoreUsingClient(redisClient, { ttl }); + const redisStore = redisStoreUsingClient(redisClient, { + ttl: this.globalConfig.cache.redis.ttl, + }); const redisCache = await caching(redisStore); @@ -54,7 +61,7 @@ export class CacheService extends TypedEmitter { return; } - const maxSize = config.getEnv('cache.memory.maxSize'); + const { maxSize, ttl } = this.globalConfig.cache.memory; const sizeCalculation = (item: unknown) => { const str = jsonStringify(item, { replaceCircularRefs: true }); diff --git a/packages/cli/src/services/communityPackages.service.ts b/packages/cli/src/services/communityPackages.service.ts index 23bf9461d697d..b1f46d52fb434 100644 --- a/packages/cli/src/services/communityPackages.service.ts +++ b/packages/cli/src/services/communityPackages.service.ts @@ -5,6 +5,7 @@ import { Service } from 'typedi'; import { promisify } from 'util'; import axios from 'axios'; +import { GlobalConfig } from '@n8n/config'; import { ApplicationError, type PublicInstalledPackage } from 'n8n-workflow'; import { InstanceSettings } from 'n8n-core'; import type { PackageDirectoryLoader } from 'n8n-core'; @@ -22,6 +23,7 @@ import { import type { CommunityPackages } from '@/Interfaces'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { Logger } from '@/Logger'; +import { OrchestrationService } from './orchestration.service'; const { PACKAGE_NAME_NOT_PROVIDED, @@ -45,6 +47,8 @@ const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/; @Service() export class CommunityPackagesService { + reinstallMissingPackages = false; + missingPackages: string[] = []; constructor( @@ -52,7 +56,11 @@ export class CommunityPackagesService { private readonly logger: Logger, private readonly installedPackageRepository: InstalledPackagesRepository, private readonly loadNodesAndCredentials: LoadNodesAndCredentials, - ) {} + private readonly orchestrationService: OrchestrationService, + globalConfig: GlobalConfig, + ) { + this.reinstallMissingPackages = globalConfig.nodes.communityPackages.reinstallMissing; + } get hasMissingPackages() { return this.missingPackages.length > 0; @@ -73,11 +81,11 @@ export class CommunityPackagesService { return await this.installedPackageRepository.find({ relations: ['installedNodes'] }); } - async removePackageFromDatabase(packageName: InstalledPackages) { + private async removePackageFromDatabase(packageName: InstalledPackages) { return await this.installedPackageRepository.remove(packageName); } - async persistInstalledPackage(packageLoader: PackageDirectoryLoader) { + private async persistInstalledPackage(packageLoader: PackageDirectoryLoader) { try { return await this.installedPackageRepository.saveInstalledPackageWithNodes(packageLoader); } catch (maybeError) { @@ -251,7 +259,7 @@ export class CommunityPackagesService { } } - async setMissingPackages({ reinstallMissingPackages }: { reinstallMissingPackages: boolean }) { + async checkForMissingPackages() { const installedPackages = await this.getAllInstalledPackages(); const missingPackages = new Set<{ packageName: string; version: string }>(); @@ -271,24 +279,24 @@ export class CommunityPackagesService { if (missingPackages.size === 0) return; - this.logger.error( - 'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/', - ); - - if (reinstallMissingPackages || process.env.N8N_REINSTALL_MISSING_PACKAGES) { + if (this.reinstallMissingPackages) { this.logger.info('Attempting to reinstall missing packages', { missingPackages }); try { // Optimistic approach - stop if any installation fails - for (const missingPackage of missingPackages) { - await this.installNpmModule(missingPackage.packageName, missingPackage.version); + await this.installPackage(missingPackage.packageName, missingPackage.version); missingPackages.delete(missingPackage); } this.logger.info('Packages reinstalled successfully. Resuming regular initialization.'); + await this.loadNodesAndCredentials.postProcessLoaders(); } catch (error) { this.logger.error('n8n was unable to install the missing packages.'); } + } else { + this.logger.warn( + 'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/', + ); } this.missingPackages = [...missingPackages].map( @@ -296,32 +304,30 @@ export class CommunityPackagesService { ); } - async installNpmModule(packageName: string, version?: string): Promise { - return await this.installOrUpdateNpmModule(packageName, { version }); + async installPackage(packageName: string, version?: string): Promise { + return await this.installOrUpdatePackage(packageName, { version }); } - async updateNpmModule( + async updatePackage( packageName: string, installedPackage: InstalledPackages, ): Promise { - return await this.installOrUpdateNpmModule(packageName, { installedPackage }); + return await this.installOrUpdatePackage(packageName, { installedPackage }); } - async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise { - await this.executeNpmCommand(`npm remove ${packageName}`); + async removePackage(packageName: string, installedPackage: InstalledPackages): Promise { + await this.removeNpmPackage(packageName); await this.removePackageFromDatabase(installedPackage); - await this.loadNodesAndCredentials.unloadPackage(packageName); - await this.loadNodesAndCredentials.postProcessLoaders(); + await this.orchestrationService.publish('community-package-uninstall', { packageName }); } - private async installOrUpdateNpmModule( + private async installOrUpdatePackage( packageName: string, options: { version?: string } | { installedPackage: InstalledPackages }, ) { const isUpdate = 'installedPackage' in options; - const command = isUpdate - ? `npm install ${packageName}@latest` - : `npm install ${packageName}${options.version ? `@${options.version}` : ''}`; + const packageVersion = isUpdate || !options.version ? 'latest' : options.version; + const command = `npm install ${packageName}@${packageVersion}`; try { await this.executeNpmCommand(command); @@ -337,9 +343,8 @@ export class CommunityPackagesService { loader = await this.loadNodesAndCredentials.loadPackage(packageName); } catch (error) { // Remove this package since loading it failed - const removeCommand = `npm remove ${packageName}`; try { - await this.executeNpmCommand(removeCommand); + await this.executeNpmCommand(`npm remove ${packageName}`); } catch {} throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error }); } @@ -351,7 +356,12 @@ export class CommunityPackagesService { await this.removePackageFromDatabase(options.installedPackage); } const installedPackage = await this.persistInstalledPackage(loader); + await this.orchestrationService.publish( + isUpdate ? 'community-package-update' : 'community-package-install', + { packageName, packageVersion }, + ); await this.loadNodesAndCredentials.postProcessLoaders(); + this.logger.info(`Community package installed: ${packageName}`); return installedPackage; } catch (error) { throw new ApplicationError('Failed to save installed package', { @@ -361,12 +371,24 @@ export class CommunityPackagesService { } } else { // Remove this package since it contains no loadable nodes - const removeCommand = `npm remove ${packageName}`; try { - await this.executeNpmCommand(removeCommand); + await this.executeNpmCommand(`npm remove ${packageName}`); } catch {} - throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES); } } + + async installOrUpdateNpmPackage(packageName: string, packageVersion: string) { + await this.executeNpmCommand(`npm install ${packageName}@${packageVersion}`); + await this.loadNodesAndCredentials.loadPackage(packageName); + await this.loadNodesAndCredentials.postProcessLoaders(); + this.logger.info(`Community package installed: ${packageName}`); + } + + async removeNpmPackage(packageName: string) { + await this.executeNpmCommand(`npm remove ${packageName}`); + await this.loadNodesAndCredentials.unloadPackage(packageName); + await this.loadNodesAndCredentials.postProcessLoaders(); + this.logger.info(`Community package uninstalled: ${packageName}`); + } } diff --git a/packages/cli/src/services/orchestration/main/handleCommandMessageMain.ts b/packages/cli/src/services/orchestration/main/handleCommandMessageMain.ts index 8abcbe78b2c47..7945f59bc35f8 100644 --- a/packages/cli/src/services/orchestration/main/handleCommandMessageMain.ts +++ b/packages/cli/src/services/orchestration/main/handleCommandMessageMain.ts @@ -10,6 +10,7 @@ import { Push } from '@/push'; import { TestWebhooks } from '@/TestWebhooks'; import { OrchestrationService } from '@/services/orchestration.service'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; +import { CommunityPackagesService } from '@/services/communityPackages.service'; // eslint-disable-next-line complexity export async function handleCommandMessageMain(messageString: string) { @@ -77,6 +78,20 @@ export async function handleCommandMessageMain(messageString: string) { } await Container.get(ExternalSecretsManager).reloadAllProviders(); break; + case 'community-package-install': + case 'community-package-update': + case 'community-package-uninstall': + if (!debounceMessageReceiver(message, 200)) { + return message; + } + const { packageName, packageVersion } = message.payload; + const communityPackagesService = Container.get(CommunityPackagesService); + if (message.command === 'community-package-uninstall') { + await communityPackagesService.removeNpmPackage(packageName); + } else { + await communityPackagesService.installOrUpdateNpmPackage(packageName, packageVersion); + } + break; case 'add-webhooks-triggers-and-pollers': { if (!debounceMessageReceiver(message, 100)) { diff --git a/packages/cli/src/services/orchestration/webhook/handleCommandMessageWebhook.ts b/packages/cli/src/services/orchestration/webhook/handleCommandMessageWebhook.ts index 9dc326978d803..e6f6e656280a8 100644 --- a/packages/cli/src/services/orchestration/webhook/handleCommandMessageWebhook.ts +++ b/packages/cli/src/services/orchestration/webhook/handleCommandMessageWebhook.ts @@ -5,6 +5,7 @@ import Container from 'typedi'; import { Logger } from 'winston'; import { messageToRedisServiceCommandObject, debounceMessageReceiver } from '../helpers'; import config from '@/config'; +import { CommunityPackagesService } from '@/services/communityPackages.service'; export async function handleCommandMessageWebhook(messageString: string) { const queueModeId = config.getEnv('redis.queueModeId'); @@ -63,6 +64,20 @@ export async function handleCommandMessageWebhook(messageString: string) { } await Container.get(ExternalSecretsManager).reloadAllProviders(); break; + case 'community-package-install': + case 'community-package-update': + case 'community-package-uninstall': + if (!debounceMessageReceiver(message, 200)) { + return message; + } + const { packageName, packageVersion } = message.payload; + const communityPackagesService = Container.get(CommunityPackagesService); + if (message.command === 'community-package-uninstall') { + await communityPackagesService.removeNpmPackage(packageName); + } else { + await communityPackagesService.installOrUpdateNpmPackage(packageName, packageVersion); + } + break; default: break; diff --git a/packages/cli/src/services/orchestration/worker/handleCommandMessageWorker.ts b/packages/cli/src/services/orchestration/worker/handleCommandMessageWorker.ts index fa9ee67675e83..23c96e1a41cd9 100644 --- a/packages/cli/src/services/orchestration/worker/handleCommandMessageWorker.ts +++ b/packages/cli/src/services/orchestration/worker/handleCommandMessageWorker.ts @@ -10,6 +10,7 @@ import { debounceMessageReceiver, getOsCpuString } from '../helpers'; import type { WorkerCommandReceivedHandlerOptions } from './types'; import { Logger } from '@/Logger'; import { N8N_VERSION } from '@/constants'; +import { CommunityPackagesService } from '@/services/communityPackages.service'; export function getWorkerCommandReceivedHandler(options: WorkerCommandReceivedHandlerOptions) { // eslint-disable-next-line complexity @@ -112,6 +113,18 @@ export function getWorkerCommandReceivedHandler(options: WorkerCommandReceivedHa }); } break; + case 'community-package-install': + case 'community-package-update': + case 'community-package-uninstall': + if (!debounceMessageReceiver(message, 500)) return; + const { packageName, packageVersion } = message.payload; + const communityPackagesService = Container.get(CommunityPackagesService); + if (message.command === 'community-package-uninstall') { + await communityPackagesService.removeNpmPackage(packageName); + } else { + await communityPackagesService.installOrUpdateNpmPackage(packageName, packageVersion); + } + break; case 'reloadLicense': if (!debounceMessageReceiver(message, 500)) return; await Container.get(License).reload(); diff --git a/packages/cli/src/services/redis/RedisServiceCommands.ts b/packages/cli/src/services/redis/RedisServiceCommands.ts index 009f39ef650eb..a8ae41c11390b 100644 --- a/packages/cli/src/services/redis/RedisServiceCommands.ts +++ b/packages/cli/src/services/redis/RedisServiceCommands.ts @@ -7,6 +7,9 @@ export type RedisServiceCommand = | 'stopWorker' | 'reloadLicense' | 'reloadExternalSecretsProviders' + | 'community-package-install' + | 'community-package-update' + | 'community-package-uninstall' | 'display-workflow-activation' // multi-main only | 'display-workflow-deactivation' // multi-main only | 'add-webhooks-triggers-and-pollers' // multi-main only @@ -26,7 +29,11 @@ export type RedisServiceBaseCommand = senderId: string; command: Exclude< RedisServiceCommand, - 'relay-execution-lifecycle-event' | 'clear-test-webhooks' + | 'relay-execution-lifecycle-event' + | 'clear-test-webhooks' + | 'community-package-install' + | 'community-package-update' + | 'community-package-uninstall' >; payload?: { [key: string]: string | number | boolean | string[] | number[] | boolean[]; @@ -41,6 +48,14 @@ export type RedisServiceBaseCommand = senderId: string; command: 'clear-test-webhooks'; payload: { webhookKey: string; workflowEntity: IWorkflowDb; pushRef: string }; + } + | { + senderId: string; + command: + | 'community-package-install' + | 'community-package-update' + | 'community-package-uninstall'; + payload: { packageName: string; packageVersion: string }; }; export type RedisServiceWorkerResponseObject = { diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index 25be080ca05bf..61f76bd596d2f 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -151,16 +151,12 @@ export class UserService { }); } - Container.get(InternalHooks).onUserInvite({ - user: owner, - target_user_id: Object.values(toInviteUsers), - public_api: false, - email_sent: result.emailSent, - invitee_role: role, // same role for all invited users - }); this.eventService.emit('user-invited', { user: owner, targetUserId: Object.values(toInviteUsers), + publicApi: false, + emailSent: result.emailSent, + inviteeRole: role, // same role for all invited users }); } catch (e) { if (e instanceof Error) { diff --git a/packages/cli/test/unit/shutdown/Shutdown.service.test.ts b/packages/cli/src/shutdown/__tests__/Shutdown.service.test.ts similarity index 100% rename from packages/cli/test/unit/shutdown/Shutdown.service.test.ts rename to packages/cli/src/shutdown/__tests__/Shutdown.service.test.ts diff --git a/packages/cli/test/unit/sso/saml/saml.service.ee.test.ts b/packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts similarity index 97% rename from packages/cli/test/unit/sso/saml/saml.service.ee.test.ts rename to packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts index 9ba6ddaf2a6bd..89d693fc98037 100644 --- a/packages/cli/test/unit/sso/saml/saml.service.ee.test.ts +++ b/packages/cli/src/sso/saml/__tests__/saml.service.ee.test.ts @@ -1,7 +1,7 @@ import { mock } from 'jest-mock-extended'; import type express from 'express'; import { SamlService } from '@/sso/saml/saml.service.ee'; -import { mockInstance } from '../../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { UrlService } from '@/services/url.service'; import { Logger } from '@/Logger'; import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify'; diff --git a/packages/cli/test/unit/sso/saml/samlHelpers.test.ts b/packages/cli/src/sso/saml/__tests__/samlHelpers.test.ts similarity index 96% rename from packages/cli/test/unit/sso/saml/samlHelpers.test.ts rename to packages/cli/src/sso/saml/__tests__/samlHelpers.test.ts index f6c35ff67e514..778b1a0857156 100644 --- a/packages/cli/test/unit/sso/saml/samlHelpers.test.ts +++ b/packages/cli/src/sso/saml/__tests__/samlHelpers.test.ts @@ -2,7 +2,7 @@ import { User } from '@/databases/entities/User'; import { generateNanoId } from '@/databases/utils/generators'; import * as helpers from '@/sso/saml/samlHelpers'; import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes'; -import { mockInstance } from '../../../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { UserRepository } from '@/databases/repositories/user.repository'; import type { AuthIdentity } from '@/databases/entities/AuthIdentity'; import { AuthIdentityRepository } from '@/databases/repositories/authIdentity.repository'; diff --git a/packages/cli/test/unit/Telemetry.test.ts b/packages/cli/src/telemetry/__tests__/telemetry.test.ts similarity index 99% rename from packages/cli/test/unit/Telemetry.test.ts rename to packages/cli/src/telemetry/__tests__/telemetry.test.ts index af8445c814663..a0441519eaf6f 100644 --- a/packages/cli/test/unit/Telemetry.test.ts +++ b/packages/cli/src/telemetry/__tests__/telemetry.test.ts @@ -1,11 +1,11 @@ import type RudderStack from '@rudderstack/rudder-sdk-node'; import { Telemetry } from '@/telemetry'; import config from '@/config'; -import { flushPromises } from './Helpers'; +import { flushPromises } from '@test/flushPromises'; import { PostHogClient } from '@/posthog'; import { mock } from 'jest-mock-extended'; import { InstanceSettings } from 'n8n-core'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; jest.unmock('@/telemetry'); jest.mock('@/posthog'); diff --git a/packages/cli/test/unit/workflow-execution.service.test.ts b/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts similarity index 100% rename from packages/cli/test/unit/workflow-execution.service.test.ts rename to packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts diff --git a/packages/cli/test/unit/services/workflowHistory.service.ee.test.ts b/packages/cli/src/workflows/workflowHistory/__tests__/workflowHistory.service.ee.test.ts similarity index 96% rename from packages/cli/test/unit/services/workflowHistory.service.ee.test.ts rename to packages/cli/src/workflows/workflowHistory/__tests__/workflowHistory.service.ee.test.ts index 05ccae70051a2..5b28b8d171468 100644 --- a/packages/cli/test/unit/services/workflowHistory.service.ee.test.ts +++ b/packages/cli/src/workflows/workflowHistory/__tests__/workflowHistory.service.ee.test.ts @@ -4,8 +4,8 @@ import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repo import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee'; import { Logger } from '@/Logger'; -import { mockInstance } from '../../shared/mocking'; -import { getWorkflow } from '../../integration/shared/workflow'; +import { mockInstance } from '@test/mocking'; +import { getWorkflow } from '@test-integration/workflow'; const workflowHistoryRepository = mockInstance(WorkflowHistoryRepository); const logger = mockInstance(Logger); diff --git a/packages/cli/test/unit/workflowHistoryHelper.test.ts b/packages/cli/src/workflows/workflowHistory/__tests__/workflowHistoryHelper.ee.test.ts similarity index 96% rename from packages/cli/test/unit/workflowHistoryHelper.test.ts rename to packages/cli/src/workflows/workflowHistory/__tests__/workflowHistoryHelper.ee.test.ts index 32a6cdd6f0d52..427f98188495b 100644 --- a/packages/cli/test/unit/workflowHistoryHelper.test.ts +++ b/packages/cli/src/workflows/workflowHistory/__tests__/workflowHistoryHelper.ee.test.ts @@ -1,7 +1,7 @@ import { License } from '@/License'; import config from '@/config'; import { getWorkflowHistoryPruneTime } from '@/workflows/workflowHistory/workflowHistoryHelper.ee'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; let licensePruneTime = -1; diff --git a/packages/cli/test/integration/PermissionChecker.test.ts b/packages/cli/test/integration/PermissionChecker.test.ts index d5262a50d0672..ff10d0497439a 100644 --- a/packages/cli/test/integration/PermissionChecker.test.ts +++ b/packages/cli/test/integration/PermissionChecker.test.ts @@ -1,6 +1,6 @@ import { v4 as uuid } from 'uuid'; import { Container } from 'typedi'; -import type { INode } from 'n8n-workflow'; +import type { INode, INodeTypeData } from 'n8n-workflow'; import { randomInt } from 'n8n-workflow'; import type { User } from '@db/entities/User'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; @@ -14,7 +14,6 @@ import { mockInstance } from '../shared/mocking'; import { randomCredentialPayload as randomCred } from '../integration/shared/random'; import * as testDb from '../integration/shared/testDb'; import type { SaveCredentialFunction } from '../integration/shared/types'; -import { mockNodeTypesData } from '../unit/Helpers'; import { affixRoleToSaveCredential } from '../integration/shared/db/credentials'; import { createOwner, createUser } from '../integration/shared/db/users'; import { SharedCredentialsRepository } from '@/databases/repositories/sharedCredentials.repository'; @@ -25,6 +24,36 @@ import { ProjectRepository } from '@/databases/repositories/project.repository'; const ownershipService = mockInstance(OwnershipService); +function mockNodeTypesData( + nodeNames: string[], + options?: { + addTrigger?: boolean; + }, +) { + return nodeNames.reduce((acc, nodeName) => { + return ( + (acc[`n8n-nodes-base.${nodeName}`] = { + sourcePath: '', + type: { + description: { + displayName: nodeName, + name: nodeName, + group: [], + description: '', + version: 1, + defaults: {}, + inputs: [], + outputs: [], + properties: [], + }, + trigger: options?.addTrigger ? async () => undefined : undefined, + }, + }), + acc + ); + }, {}); +} + const createWorkflow = async (nodes: INode[], workflowOwner?: User): Promise => { const workflowDetails = { id: randomInt(1, 10).toString(), diff --git a/packages/cli/test/integration/activation-errors.service.test.ts b/packages/cli/test/integration/activation-errors.service.test.ts index 7635660db3aca..5d56f0dc90d6a 100644 --- a/packages/cli/test/integration/activation-errors.service.test.ts +++ b/packages/cli/test/integration/activation-errors.service.test.ts @@ -1,8 +1,13 @@ import { ActivationErrorsService } from '@/ActivationErrors.service'; import { CacheService } from '@/services/cache/cache.service'; +import { GlobalConfig } from '@n8n/config'; +import { mockInstance } from '@test/mocking'; describe('ActivationErrorsService', () => { - const cacheService = new CacheService(); + const globalConfig = mockInstance(GlobalConfig, { + cache: { backend: 'memory', memory: { maxSize: 3 * 1024 * 1024, ttl: 3600 * 1000 } }, + }); + const cacheService = new CacheService(globalConfig); const activationErrorsService = new ActivationErrorsService(cacheService); const firstWorkflowId = 'GSG0etbfTA2CNPDX'; diff --git a/packages/cli/test/integration/community-packages.api.test.ts b/packages/cli/test/integration/community-packages.api.test.ts index 661dbcd0fe4e9..46c7efad03434 100644 --- a/packages/cli/test/integration/community-packages.api.test.ts +++ b/packages/cli/test/integration/community-packages.api.test.ts @@ -179,7 +179,7 @@ describe('POST /community-packages', () => { communityPackagesService.hasPackageLoaded.mockReturnValue(false); communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' }); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); - communityPackagesService.installNpmModule.mockResolvedValue(mockPackage()); + communityPackagesService.installPackage.mockResolvedValue(mockPackage()); await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(200); @@ -219,7 +219,7 @@ describe('DELETE /community-packages', () => { await authAgent.delete('/community-packages').query({ name: mockPackageName() }).expect(200); - expect(communityPackagesService.removeNpmModule).toHaveBeenCalledTimes(1); + expect(communityPackagesService.removePackage).toHaveBeenCalledTimes(1); }); }); @@ -242,6 +242,6 @@ describe('PATCH /community-packages', () => { await authAgent.patch('/community-packages').send({ name: mockPackageName() }); - expect(communityPackagesService.updateNpmModule).toHaveBeenCalledTimes(1); + expect(communityPackagesService.updatePackage).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/cli/test/integration/prometheus-metrics.test.ts b/packages/cli/test/integration/prometheus-metrics.test.ts index 9f6a0fad6e89d..1eccb9b7d0ce2 100644 --- a/packages/cli/test/integration/prometheus-metrics.test.ts +++ b/packages/cli/test/integration/prometheus-metrics.test.ts @@ -12,8 +12,8 @@ jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); const toLines = (response: Response) => response.text.trim().split('\n'); const globalConfig = Container.get(GlobalConfig); -// @ts-expect-error `metrics` is a readonly property globalConfig.endpoints.metrics = { + enable: true, prefix: 'n8n_test_', includeDefaultMetrics: true, includeApiEndpoints: true, diff --git a/packages/cli/test/unit/webhooks.test.ts b/packages/cli/test/integration/webhooks.test.ts similarity index 98% rename from packages/cli/test/unit/webhooks.test.ts rename to packages/cli/test/integration/webhooks.test.ts index c891588597ac5..9bd1977ed53d2 100644 --- a/packages/cli/test/unit/webhooks.test.ts +++ b/packages/cli/test/integration/webhooks.test.ts @@ -11,7 +11,7 @@ import { WaitingWebhooks } from '@/WaitingWebhooks'; import { WaitingForms } from '@/WaitingForms'; import type { IResponseCallbackData } from '@/Interfaces'; -import { mockInstance } from '../shared/mocking'; +import { mockInstance } from '@test/mocking'; import { GlobalConfig } from '@n8n/config'; import Container from 'typedi'; diff --git a/packages/cli/test/shared/flushPromises.ts b/packages/cli/test/shared/flushPromises.ts new file mode 100644 index 0000000000000..405b9e98dca26 --- /dev/null +++ b/packages/cli/test/shared/flushPromises.ts @@ -0,0 +1,6 @@ +/** + * Ensure all pending promises settle. The promise's `resolve` is placed in + * the macrotask queue and so called at the next iteration of the event loop + * after all promises in the microtask queue have settled first. + */ +export const flushPromises = async () => await new Promise(setImmediate); diff --git a/packages/cli/test/unit/shared/mockObjects.ts b/packages/cli/test/shared/mockObjects.ts similarity index 94% rename from packages/cli/test/unit/shared/mockObjects.ts rename to packages/cli/test/shared/mockObjects.ts index e7a165977301f..a8795e8e1013f 100644 --- a/packages/cli/test/unit/shared/mockObjects.ts +++ b/packages/cli/test/shared/mockObjects.ts @@ -8,7 +8,7 @@ import { randomEmail, randomName, uniqueId, -} from '../../integration/shared/random'; +} from '../integration/shared/random'; export const mockCredential = (): CredentialsEntity => Object.assign(new CredentialsEntity(), randomCredentialPayload()); diff --git a/packages/cli/test/unit/shared/testData.ts b/packages/cli/test/shared/testData.ts similarity index 100% rename from packages/cli/test/unit/shared/testData.ts rename to packages/cli/test/shared/testData.ts diff --git a/packages/cli/test/unit/Helpers.ts b/packages/cli/test/unit/Helpers.ts deleted file mode 100644 index 50b9f43489076..0000000000000 --- a/packages/cli/test/unit/Helpers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { INodeTypeData } from 'n8n-workflow'; - -/** - * Ensure all pending promises settle. The promise's `resolve` is placed in - * the macrotask queue and so called at the next iteration of the event loop - * after all promises in the microtask queue have settled first. - */ -export const flushPromises = async () => await new Promise(setImmediate); - -export function mockNodeTypesData( - nodeNames: string[], - options?: { - addTrigger?: boolean; - }, -) { - return nodeNames.reduce((acc, nodeName) => { - return ( - (acc[`n8n-nodes-base.${nodeName}`] = { - sourcePath: '', - type: { - description: { - displayName: nodeName, - name: nodeName, - group: [], - description: '', - version: 1, - defaults: {}, - inputs: [], - outputs: [], - properties: [], - }, - trigger: options?.addTrigger ? async () => undefined : undefined, - }, - }), - acc - ); - }, {}); -} diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 770cfe2298238..8c93c847ba540 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1808,8 +1808,8 @@ export type AddedNode = { } & Partial; export type AddedNodeConnection = { - from: { nodeIndex: number; outputIndex?: number }; - to: { nodeIndex: number; inputIndex?: number }; + from: { nodeIndex: number; outputIndex?: number; type?: NodeConnectionType }; + to: { nodeIndex: number; inputIndex?: number; type?: NodeConnectionType }; }; export type AddedNodesAndConnections = { diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts index 2346968f798ab..e9f7917385eec 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts @@ -1,5 +1,10 @@ import { computed } from 'vue'; -import type { IDataObject, INodeParameters } from 'n8n-workflow'; +import { + CHAIN_LLM_LANGCHAIN_NODE_TYPE, + NodeConnectionType, + type IDataObject, + type INodeParameters, +} from 'n8n-workflow'; import type { ActionTypeDescription, AddedNode, @@ -11,6 +16,7 @@ import type { } from '@/Interface'; import { AGENT_NODE_TYPE, + AI_CATEGORY_LANGUAGE_MODELS, BASIC_CHAIN_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE, @@ -37,11 +43,12 @@ import { useExternalHooks } from '@/composables/useExternalHooks'; import { sortNodeCreateElements, transformNodeType } from '../utils'; import { useI18n } from '@/composables/useI18n'; +import { useCanvasStore } from '@/stores/canvas.store'; export const useActions = () => { const nodeCreatorStore = useNodeCreatorStore(); + const nodeTypesStore = useNodeTypesStore(); const i18n = useI18n(); - const singleNodeOpenSources = [ NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT, NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION, @@ -216,6 +223,19 @@ export const useActions = () => { return isCompatibleNode && isChatTriggerMissing; } + // AI-226: Prepend LLM Chain node when adding a language model + function shouldPrependLLMChain(addedNodes: AddedNode[]): boolean { + const canvasHasAINodes = useCanvasStore().aiNodes.length > 0; + if (canvasHasAINodes) return false; + + return addedNodes.some((node) => { + const nodeType = nodeTypesStore.getNodeType(node.type); + return Object.keys(nodeType?.codex?.subcategories ?? {}).includes( + AI_CATEGORY_LANGUAGE_MODELS, + ); + }); + } + function getAddedNodesAndConnections(addedNodes: AddedNode[]): AddedNodesAndConnections { if (addedNodes.length === 0) { return { nodes: [], connections: [] }; @@ -230,7 +250,14 @@ export const useActions = () => { nodeToAutoOpen.openDetail = true; } - if (shouldPrependChatTrigger(addedNodes)) { + if (shouldPrependLLMChain(addedNodes) || shouldPrependChatTrigger(addedNodes)) { + if (shouldPrependLLMChain(addedNodes)) { + addedNodes.unshift({ type: CHAIN_LLM_LANGCHAIN_NODE_TYPE, isAutoAdd: true }); + connections.push({ + from: { nodeIndex: 2, type: NodeConnectionType.AiLanguageModel }, + to: { nodeIndex: 1 }, + }); + } addedNodes.unshift({ type: CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true }); connections.push({ from: { nodeIndex: 0 }, diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 5a5e1a1aaae7f..e837096e46f73 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -92,7 +92,6 @@ export const NPM_KEYWORD_SEARCH_URL = 'https://www.npmjs.com/search?q=keywords%3An8n-community-node-package'; export const N8N_QUEUE_MODE_DOCS_URL = `https://${DOCS_DOMAIN}/hosting/scaling/queue-mode/`; export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/gui-install/`; -export const COMMUNITY_NODES_MANUAL_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/manual-install/`; export const COMMUNITY_NODES_NPM_INSTALLATION_URL = 'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm'; export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/risks/`; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 672606218d5c8..60c7920d5ce1a 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1569,7 +1569,6 @@ "settings.communityNodes.empty.description": "Install over {count} node packages contributed by our community.", "settings.communityNodes.empty.description.no-packages": "Install node packages contributed by our community.", "settings.communityNodes.empty.installPackageLabel": "Install a community node", - "settings.communityNodes.queueMode.warning": "You need to install community nodes manually because your instance is running in queue mode. More info", "settings.communityNodes.npmUnavailable.warning": "To use this feature, please install npm and restart n8n.", "settings.communityNodes.notAvailableOnDesktop": "Feature unavailable on desktop. Please self-host to use community nodes.", "settings.communityNodes.packageNodes.label": "{count} node | {count} nodes", diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index ec0458e1539bd..93c8ab2671e22 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -610,7 +610,7 @@ export default defineComponent({ return this.workflowsStore.getWorkflowExecution; }, workflowRunning(): boolean { - return this.uiStore.isActionActive['workflowRunning']; + return this.uiStore.isActionActive.workflowRunning; }, currentWorkflow(): string { return this.$route.params.name?.toString() || this.workflowsStore.workflowId; @@ -4428,7 +4428,7 @@ export default defineComponent({ from.outputIndex ?? 0, toNode.name, to.inputIndex ?? 0, - NodeConnectionType.Main, + from.type ?? NodeConnectionType.Main, ); } @@ -4449,6 +4449,22 @@ export default defineComponent({ }); } + const lastNodeType = this.nodeTypesStore.getNodeType(lastAddedNode.type); + const isSubNode = NodeHelpers.isSubNodeType(lastNodeType); + + // When adding a sub-node and there's more than one node added at the time, it must mean that it's + // connected to a root node, so we adjust the position of the sub-node to make it appear in the correct + // in relation to the root node + if (isSubNode && nodes.length > 1) { + this.onMoveNode({ + nodeName: lastAddedNode.name, + position: [ + lastAddedNode.position[0] - NodeViewUtils.NODE_SIZE * 2.5, + lastAddedNode.position[1] + NodeViewUtils.NODE_SIZE * 1.5, + ], + }); + } + this.nodeHelpers.addPinDataConnections(this.workflowsStore.pinnedWorkflowData); }, diff --git a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue index fcfb3742383c6..194a289eb1b9b 100644 --- a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue +++ b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue @@ -3,25 +3,13 @@
{{ $locale.baseText('settings.communityNodes') }}
-
- -
-
+