From 45813debc963096f63cc0aabe82d9d9f853a39d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 14 Aug 2024 10:55:13 +0200 Subject: [PATCH 01/22] fix(core): Fix duplicate Redis publisher (#10392) --- packages/cli/src/services/redis/RedisServiceBaseClasses.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/src/services/redis/RedisServiceBaseClasses.ts b/packages/cli/src/services/redis/RedisServiceBaseClasses.ts index ba2cc41a91dce..d694a1a774ee6 100644 --- a/packages/cli/src/services/redis/RedisServiceBaseClasses.ts +++ b/packages/cli/src/services/redis/RedisServiceBaseClasses.ts @@ -45,6 +45,8 @@ class RedisServiceBase { this.logger.warn('Error with Redis: ', error); } }); + + this.isInitialized = true; } async destroy(): Promise { From e640f1f42cad41d9849c2b3c057a9f8d0cb43b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 14 Aug 2024 11:16:02 +0200 Subject: [PATCH 02/22] test(core): Cover telemetry event relay in tests (no-changelog) (#10391) --- .../__tests__/telemetry-event-relay.test.ts | 1030 +++++++++++++++++ 1 file changed, 1030 insertions(+) create mode 100644 packages/cli/src/events/__tests__/telemetry-event-relay.test.ts diff --git a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts new file mode 100644 index 0000000000000..e1841f172633a --- /dev/null +++ b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts @@ -0,0 +1,1030 @@ +import { mock } from 'jest-mock-extended'; +import { TelemetryEventRelay } from '@/events/telemetry-event-relay'; +import { EventService } from '@/events/event.service'; +import config from '@/config'; +import type { IWorkflowBase } from 'n8n-workflow'; +import type { IWorkflowDb } from '@/Interfaces'; +import type { Telemetry } from '@/telemetry'; +import type { License } from '@/License'; +import type { GlobalConfig } from '@n8n/config'; +import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; +import type { NodeTypes } from '@/NodeTypes'; +import type { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository'; +import type { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository'; +import type { RelayEventMap } from '@/events/relay-event-map'; +import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; + +const flushPromises = async () => await new Promise((resolve) => setImmediate(resolve)); + +describe('TelemetryEventRelay', () => { + const telemetry = mock(); + const license = mock(); + const globalConfig = mock({ userManagement: { emails: { mode: 'smtp' } } }); + const workflowRepository = mock(); + const nodeTypes = mock(); + const sharedWorkflowRepository = mock(); + const projectRelationRepository = mock(); + const eventService = new EventService(); + + let telemetryEventRelay: TelemetryEventRelay; + + beforeAll(async () => { + telemetryEventRelay = new TelemetryEventRelay( + eventService, + telemetry, + license, + globalConfig, + workflowRepository, + nodeTypes, + sharedWorkflowRepository, + projectRelationRepository, + ); + + await telemetryEventRelay.init(); + }); + + beforeEach(() => { + config.set('diagnostics.enabled', true); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('init', () => { + it('with diagnostics enabled, should init telemetry and register listeners', async () => { + config.set('diagnostics.enabled', true); + const telemetryEventRelay = new TelemetryEventRelay( + eventService, + telemetry, + license, + globalConfig, + workflowRepository, + nodeTypes, + sharedWorkflowRepository, + projectRelationRepository, + ); + // @ts-expect-error Private method + const setupListenersSpy = jest.spyOn(telemetryEventRelay, 'setupListeners'); + + await telemetryEventRelay.init(); + + expect(telemetry.init).toHaveBeenCalled(); + expect(setupListenersSpy).toHaveBeenCalled(); + }); + + it('with diagnostics disabled, should neither init telemetry nor register listeners', async () => { + config.set('diagnostics.enabled', false); + const telemetryEventRelay = new TelemetryEventRelay( + eventService, + telemetry, + license, + globalConfig, + workflowRepository, + nodeTypes, + sharedWorkflowRepository, + projectRelationRepository, + ); + // @ts-expect-error Private method + const setupListenersSpy = jest.spyOn(telemetryEventRelay, 'setupListeners'); + + await telemetryEventRelay.init(); + + expect(telemetry.init).not.toHaveBeenCalled(); + expect(setupListenersSpy).not.toHaveBeenCalled(); + }); + }); + + describe('project events', () => { + it('should track on `team-project-updated` event', () => { + const event: RelayEventMap['team-project-updated'] = { + userId: 'user123', + role: 'global:owner', + members: [ + { userId: 'user456', role: 'project:admin' }, + { userId: 'user789', role: 'project:editor' }, + ], + projectId: 'project123', + }; + + eventService.emit('team-project-updated', event); + + expect(telemetry.track).toHaveBeenCalledWith('Project settings updated', { + user_id: 'user123', + role: 'global:owner', + members: [ + { user_id: 'user456', role: 'project:admin' }, + { user_id: 'user789', role: 'project:editor' }, + ], + project_id: 'project123', + }); + }); + + it('should track on `team-project-deleted` event', () => { + const event: RelayEventMap['team-project-deleted'] = { + userId: 'user123', + role: 'global:owner', + projectId: 'project123', + removalType: 'delete', + }; + + eventService.emit('team-project-deleted', event); + + expect(telemetry.track).toHaveBeenCalledWith('User deleted project', { + user_id: 'user123', + role: 'global:owner', + project_id: 'project123', + removal_type: 'delete', + target_project_id: undefined, + }); + }); + + it('should track on `team-project-created` event', () => { + const event: RelayEventMap['team-project-created'] = { + userId: 'user123', + role: 'global:owner', + }; + + eventService.emit('team-project-created', event); + + expect(telemetry.track).toHaveBeenCalledWith('User created project', { + user_id: 'user123', + role: 'global:owner', + }); + }); + }); + + describe('source control events', () => { + it('should track on `source-control-settings-updated` event', () => { + const event: RelayEventMap['source-control-settings-updated'] = { + branchName: 'main', + readOnlyInstance: false, + repoType: 'github', + connected: true, + }; + + eventService.emit('source-control-settings-updated', event); + + expect(telemetry.track).toHaveBeenCalledWith('User updated source control settings', { + branch_name: 'main', + read_only_instance: false, + repo_type: 'github', + connected: true, + }); + }); + + it('should track on `source-control-user-started-pull-ui` event', () => { + const event: RelayEventMap['source-control-user-started-pull-ui'] = { + workflowUpdates: 5, + workflowConflicts: 2, + credConflicts: 1, + }; + + eventService.emit('source-control-user-started-pull-ui', event); + + expect(telemetry.track).toHaveBeenCalledWith('User started pull via UI', { + workflow_updates: 5, + workflow_conflicts: 2, + cred_conflicts: 1, + }); + }); + + it('should track on `source-control-user-finished-pull-ui` event', () => { + const event: RelayEventMap['source-control-user-finished-pull-ui'] = { + workflowUpdates: 3, + }; + + eventService.emit('source-control-user-finished-pull-ui', event); + + expect(telemetry.track).toHaveBeenCalledWith('User finished pull via UI', { + workflow_updates: 3, + }); + }); + + it('should track on `source-control-user-pulled-api` event', () => { + const event: RelayEventMap['source-control-user-pulled-api'] = { + workflowUpdates: 2, + forced: false, + }; + + eventService.emit('source-control-user-pulled-api', event); + + expect(telemetry.track).toHaveBeenCalledWith('User pulled via API', { + workflow_updates: 2, + forced: false, + }); + }); + + it('should track on `source-control-user-started-push-ui` event', () => { + const event: RelayEventMap['source-control-user-started-push-ui'] = { + workflowsEligible: 10, + workflowsEligibleWithConflicts: 2, + credsEligible: 5, + credsEligibleWithConflicts: 1, + variablesEligible: 3, + }; + + eventService.emit('source-control-user-started-push-ui', event); + + expect(telemetry.track).toHaveBeenCalledWith('User started push via UI', { + workflows_eligible: 10, + workflows_eligible_with_conflicts: 2, + creds_eligible: 5, + creds_eligible_with_conflicts: 1, + variables_eligible: 3, + }); + }); + + it('should track on `source-control-user-finished-push-ui` event', () => { + const event: RelayEventMap['source-control-user-finished-push-ui'] = { + workflowsEligible: 10, + workflowsPushed: 8, + credsPushed: 5, + variablesPushed: 3, + }; + + eventService.emit('source-control-user-finished-push-ui', event); + + expect(telemetry.track).toHaveBeenCalledWith('User finished push via UI', { + workflows_eligible: 10, + workflows_pushed: 8, + creds_pushed: 5, + variables_pushed: 3, + }); + }); + }); + + describe('license events', () => { + it('should track on `license-renewal-attempted` event', () => { + const event: RelayEventMap['license-renewal-attempted'] = { + success: true, + }; + + eventService.emit('license-renewal-attempted', event); + + expect(telemetry.track).toHaveBeenCalledWith('Instance attempted to refresh license', { + success: true, + }); + }); + }); + + describe('variable events', () => { + it('should track on `variable-created` event', () => { + eventService.emit('variable-created', {}); + + expect(telemetry.track).toHaveBeenCalledWith('User created variable'); + }); + }); + + describe('external secrets events', () => { + it('should track on `external-secrets-provider-settings-saved` event', () => { + const event: RelayEventMap['external-secrets-provider-settings-saved'] = { + userId: 'user123', + vaultType: 'aws', + isValid: true, + isNew: false, + }; + + eventService.emit('external-secrets-provider-settings-saved', event); + + expect(telemetry.track).toHaveBeenCalledWith('User updated external secrets settings', { + user_id: 'user123', + vault_type: 'aws', + is_valid: true, + is_new: false, + error_message: undefined, + }); + }); + }); + + describe('public API events', () => { + it('should track on `public-api-invoked` event', () => { + const event: RelayEventMap['public-api-invoked'] = { + userId: 'user123', + path: '/api/v1/workflows', + method: 'GET', + apiVersion: 'v1', + }; + + eventService.emit('public-api-invoked', event); + + expect(telemetry.track).toHaveBeenCalledWith('User invoked API', { + user_id: 'user123', + path: '/api/v1/workflows', + method: 'GET', + api_version: 'v1', + }); + }); + + it('should track on `public-api-key-created` event', () => { + const event: RelayEventMap['public-api-key-created'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + publicApi: true, + }; + + eventService.emit('public-api-key-created', event); + + expect(telemetry.track).toHaveBeenCalledWith('API key created', { + user_id: 'user123', + public_api: true, + }); + }); + + it('should track on `public-api-key-deleted` event', () => { + const event: RelayEventMap['public-api-key-deleted'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + publicApi: true, + }; + + eventService.emit('public-api-key-deleted', event); + + expect(telemetry.track).toHaveBeenCalledWith('API key deleted', { + user_id: 'user123', + public_api: true, + }); + }); + }); + + describe('community package events', () => { + it('should track on `community-package-installed` event', () => { + const event: RelayEventMap['community-package-installed'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + inputString: 'n8n-nodes-package', + packageName: 'n8n-nodes-package', + success: true, + packageVersion: '1.0.0', + packageNodeNames: ['CustomNode1', 'CustomNode2'], + packageAuthor: 'John Smith', + packageAuthorEmail: 'john@example.com', + }; + + eventService.emit('community-package-installed', event); + + expect(telemetry.track).toHaveBeenCalledWith('cnr package install finished', { + user_id: 'user123', + input_string: 'n8n-nodes-package', + package_name: 'n8n-nodes-package', + success: true, + package_version: '1.0.0', + package_node_names: ['CustomNode1', 'CustomNode2'], + package_author: 'John Smith', + package_author_email: 'john@example.com', + failure_reason: undefined, + }); + }); + + it('should track on `community-package-updated` event', () => { + const event: RelayEventMap['community-package-updated'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + packageName: 'n8n-nodes-package', + packageVersionCurrent: '1.0.0', + packageVersionNew: '1.1.0', + packageNodeNames: ['CustomNode1', 'CustomNode2'], + packageAuthor: 'John Smith', + packageAuthorEmail: 'john@example.com', + }; + + eventService.emit('community-package-updated', event); + + expect(telemetry.track).toHaveBeenCalledWith('cnr package updated', { + user_id: 'user123', + package_name: 'n8n-nodes-package', + package_version_current: '1.0.0', + package_version_new: '1.1.0', + package_node_names: ['CustomNode1', 'CustomNode2'], + package_author: 'John Smith', + package_author_email: 'john@example.com', + }); + }); + + it('should track on `community-package-deleted` event', () => { + const event: RelayEventMap['community-package-deleted'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + packageName: 'n8n-nodes-package', + packageVersion: '1.0.0', + packageNodeNames: ['CustomNode1', 'CustomNode2'], + packageAuthor: 'John Smith', + packageAuthorEmail: 'john@example.com', + }; + + eventService.emit('community-package-deleted', event); + + expect(telemetry.track).toHaveBeenCalledWith('cnr package deleted', { + user_id: 'user123', + package_name: 'n8n-nodes-package', + package_version: '1.0.0', + package_node_names: ['CustomNode1', 'CustomNode2'], + package_author: 'John Smith', + package_author_email: 'john@example.com', + }); + }); + }); + + describe('credentials events', () => { + it('should track on `credentials-created` event', () => { + const event: RelayEventMap['credentials-created'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + credentialType: 'github', + credentialId: 'cred123', + publicApi: false, + projectId: 'project123', + projectType: 'personal', + }; + + eventService.emit('credentials-created', event); + + expect(telemetry.track).toHaveBeenCalledWith('User created credentials', { + user_id: 'user123', + credential_type: 'github', + credential_id: 'cred123', + project_id: 'project123', + project_type: 'personal', + }); + }); + + it('should track on `credentials-shared` event', () => { + const event: RelayEventMap['credentials-shared'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + credentialType: 'github', + credentialId: 'cred123', + userIdSharer: 'user123', + userIdsShareesAdded: ['user456', 'user789'], + shareesRemoved: 1, + }; + + eventService.emit('credentials-shared', event); + + expect(telemetry.track).toHaveBeenCalledWith('User updated cred sharing', { + user_id: 'user123', + credential_type: 'github', + credential_id: 'cred123', + user_id_sharer: 'user123', + user_ids_sharees_added: ['user456', 'user789'], + sharees_removed: 1, + }); + }); + + it('should track on `credentials-updated` event', () => { + const event: RelayEventMap['credentials-updated'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + credentialId: 'cred123', + credentialType: 'github', + }; + + eventService.emit('credentials-updated', event); + + expect(telemetry.track).toHaveBeenCalledWith('User updated credentials', { + user_id: 'user123', + credential_type: 'github', + credential_id: 'cred123', + }); + }); + + it('should track on `credentials-deleted` event', () => { + const event: RelayEventMap['credentials-deleted'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + credentialId: 'cred123', + credentialType: 'github', + }; + + eventService.emit('credentials-deleted', event); + + expect(telemetry.track).toHaveBeenCalledWith('User deleted credentials', { + user_id: 'user123', + credential_type: 'github', + credential_id: 'cred123', + }); + }); + }); + + describe('LDAP events', () => { + it('should track on `ldap-general-sync-finished` event', () => { + const event: RelayEventMap['ldap-general-sync-finished'] = { + type: 'full', + succeeded: true, + usersSynced: 10, + error: '', + }; + + eventService.emit('ldap-general-sync-finished', event); + + expect(telemetry.track).toHaveBeenCalledWith('Ldap general sync finished', { + type: 'full', + succeeded: true, + users_synced: 10, + error: '', + }); + }); + + it('should track on `ldap-settings-updated` event', () => { + const event: RelayEventMap['ldap-settings-updated'] = { + userId: 'user123', + loginIdAttribute: 'uid', + firstNameAttribute: 'givenName', + lastNameAttribute: 'sn', + emailAttribute: 'mail', + ldapIdAttribute: 'entryUUID', + searchPageSize: 100, + searchTimeout: 60, + synchronizationEnabled: true, + synchronizationInterval: 60, + loginLabel: 'LDAP Login', + loginEnabled: true, + }; + + eventService.emit('ldap-settings-updated', { + ...event, + }); + + const { userId: _, ...rest } = event; + + expect(telemetry.track).toHaveBeenCalledWith('User updated Ldap settings', { + user_id: 'user123', + ...rest, + }); + }); + + it('should track on `ldap-login-sync-failed` event', () => { + const event: RelayEventMap['ldap-login-sync-failed'] = { + error: 'Connection failed', + }; + + eventService.emit('ldap-login-sync-failed', event); + + expect(telemetry.track).toHaveBeenCalledWith('Ldap login sync failed', { + error: 'Connection failed', + }); + }); + + it('should track on `login-failed-due-to-ldap-disabled` event', () => { + const event: RelayEventMap['login-failed-due-to-ldap-disabled'] = { + userId: 'user123', + }; + + eventService.emit('login-failed-due-to-ldap-disabled', event); + + expect(telemetry.track).toHaveBeenCalledWith('User login failed since ldap disabled', { + user_ud: 'user123', + }); + }); + }); + + describe('workflow events', () => { + it('should track on `workflow-created` event', async () => { + const event: RelayEventMap['workflow-created'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + workflow: mock({ id: 'workflow123', name: 'Test Workflow', nodes: [] }), + publicApi: false, + projectId: 'project123', + projectType: 'personal', + }; + + eventService.emit('workflow-created', event); + + await flushPromises(); + + expect(telemetry.track).toHaveBeenCalledWith('User created workflow', { + user_id: 'user123', + workflow_id: 'workflow123', + node_graph_string: expect.any(String), + public_api: false, + project_id: 'project123', + project_type: 'personal', + }); + }); + + it('should track on `workflow-deleted` event', () => { + const event: RelayEventMap['workflow-deleted'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + workflowId: 'workflow123', + publicApi: false, + }; + + eventService.emit('workflow-deleted', event); + + expect(telemetry.track).toHaveBeenCalledWith('User deleted workflow', { + user_id: 'user123', + workflow_id: 'workflow123', + public_api: false, + }); + }); + + it('should track on `workflow-post-execute` event', async () => { + const event: RelayEventMap['workflow-post-execute'] = { + workflow: mock({ + id: 'workflow123', + name: 'Test Workflow', + nodes: [], + }), + userId: 'user123', + executionId: 'execution123', + }; + + eventService.emit('workflow-post-execute', event); + + await flushPromises(); + + expect(telemetry.trackWorkflowExecution).toHaveBeenCalledWith({ + is_manual: false, + success: false, + user_id: 'user123', + version_cli: '1.54.0', + workflow_id: 'workflow123', + }); + }); + + it('should track on `workflow-saved` event', async () => { + const event: RelayEventMap['workflow-saved'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + workflow: mock({ id: 'workflow123', name: 'Test Workflow', nodes: [] }), + publicApi: false, + }; + + eventService.emit('workflow-saved', event); + + await flushPromises(); + + expect(telemetry.track).toHaveBeenCalledWith('User saved workflow', { + user_id: 'user123', + workflow_id: 'workflow123', + node_graph_string: expect.any(String), + notes_count_overlapping: 0, + notes_count_non_overlapping: 0, + version_cli: expect.any(String), + num_tags: 0, + public_api: false, + sharing_role: undefined, + }); + }); + + it('should track on `workflow-sharing-updated` event', () => { + const event: RelayEventMap['workflow-sharing-updated'] = { + workflowId: 'workflow123', + userIdSharer: 'user123', + userIdList: ['user456', 'user789'], + }; + + eventService.emit('workflow-sharing-updated', event); + + expect(telemetry.track).toHaveBeenCalledWith('User updated workflow sharing', { + workflow_id: 'workflow123', + user_id_sharer: 'user123', + user_id_list: ['user456', 'user789'], + }); + }); + }); + + describe('user events', () => { + it('should track on `user-updated` event', () => { + const event: RelayEventMap['user-updated'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + fieldsChanged: ['firstName', 'lastName'], + }; + + eventService.emit('user-updated', event); + + expect(telemetry.track).toHaveBeenCalledWith('User changed personal settings', { + user_id: 'user123', + fields_changed: ['firstName', 'lastName'], + }); + }); + + it('should track on `user-deleted` event', () => { + const event: RelayEventMap['user-deleted'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + publicApi: false, + targetUserOldStatus: 'active', + migrationStrategy: 'transfer_data', + targetUserId: 'user456', + migrationUserId: 'user789', + }; + + eventService.emit('user-deleted', event); + + expect(telemetry.track).toHaveBeenCalledWith('User deleted user', { + user_id: 'user123', + public_api: false, + target_user_old_status: 'active', + migration_strategy: 'transfer_data', + target_user_id: 'user456', + migration_user_id: 'user789', + }); + }); + + it('should track on `user-invited` event', () => { + const event: RelayEventMap['user-invited'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + targetUserId: ['user456'], + publicApi: false, + emailSent: true, + inviteeRole: 'global:member', + }; + + eventService.emit('user-invited', event); + + expect(telemetry.track).toHaveBeenCalledWith('User invited new user', { + user_id: 'user123', + target_user_id: ['user456'], + public_api: false, + email_sent: true, + invitee_role: 'global:member', + }); + }); + + it('should track on `user-signed-up` event', () => { + const event: RelayEventMap['user-signed-up'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + userType: 'email', + wasDisabledLdapUser: false, + }; + + eventService.emit('user-signed-up', event); + + expect(telemetry.track).toHaveBeenCalledWith('User signed up', { + user_id: 'user123', + user_type: 'email', + was_disabled_ldap_user: false, + }); + }); + + it('should track on `user-submitted-personalization-survey` event', () => { + const event: RelayEventMap['user-submitted-personalization-survey'] = { + userId: 'user123', + answers: { + companySize: '1-10', + workArea: 'IT', + automationGoal: 'Improve efficiency', + valueExpectation: 'Time savings', + }, + }; + + eventService.emit('user-submitted-personalization-survey', event); + + expect(telemetry.track).toHaveBeenCalledWith('User responded to personalization questions', { + user_id: 'user123', + company_size: '1-10', + work_area: 'IT', + automation_goal: 'Improve efficiency', + value_expectation: 'Time savings', + }); + }); + + it('should track on `user-changed-role` event', () => { + const event: RelayEventMap['user-changed-role'] = { + userId: 'user123', + targetUserId: 'user456', + targetUserNewRole: 'global:member', + publicApi: false, + }; + + eventService.emit('user-changed-role', event); + + expect(telemetry.track).toHaveBeenCalledWith('User changed role', { + user_id: 'user123', + target_user_id: 'user456', + target_user_new_role: 'global:member', + public_api: false, + }); + }); + + it('should track on `user-retrieved-user` event', () => { + const event: RelayEventMap['user-retrieved-user'] = { + userId: 'user123', + publicApi: false, + }; + + eventService.emit('user-retrieved-user', event); + + expect(telemetry.track).toHaveBeenCalledWith('User retrieved user', { + user_id: 'user123', + public_api: false, + }); + }); + + it('should track on `user-retrieved-all-users` event', () => { + const event: RelayEventMap['user-retrieved-all-users'] = { + userId: 'user123', + publicApi: false, + }; + + eventService.emit('user-retrieved-all-users', event); + + expect(telemetry.track).toHaveBeenCalledWith('User retrieved all users', { + user_id: 'user123', + public_api: false, + }); + }); + }); + + describe('lifecycle events', () => { + it('should track on `server-started` event', async () => { + const firstWorkflow = mock({ createdAt: new Date() }); + workflowRepository.findOne.mockResolvedValue(firstWorkflow); + + eventService.emit('server-started'); + + await flushPromises(); + + // expect(telemetry.identify).toHaveBeenCalled(); + expect(telemetry.track).toHaveBeenCalledWith( + 'Instance started', + expect.objectContaining({ + earliest_workflow_created: firstWorkflow.createdAt, + }), + ); + }); + + it('should track on `session-started` event', () => { + const event: RelayEventMap['session-started'] = { + pushRef: 'ref123', + }; + + eventService.emit('session-started', event); + + expect(telemetry.track).toHaveBeenCalledWith('Session started', { + session_id: 'ref123', + }); + }); + + it('should track on `instance-stopped` event', () => { + eventService.emit('instance-stopped', {}); + + expect(telemetry.track).toHaveBeenCalledWith('User instance stopped'); + }); + + it('should track on `instance-owner-setup` event', () => { + const event: RelayEventMap['instance-owner-setup'] = { + userId: 'user123', + }; + + eventService.emit('instance-owner-setup', event); + + expect(telemetry.track).toHaveBeenCalledWith('Owner finished instance setup', { + user_id: 'user123', + }); + }); + }); + + describe('workflow execution events', () => { + it('should track on `first-production-workflow-succeeded` event', () => { + const event: RelayEventMap['first-production-workflow-succeeded'] = { + projectId: 'project123', + workflowId: 'workflow123', + userId: 'user123', + }; + + eventService.emit('first-production-workflow-succeeded', event); + + expect(telemetry.track).toHaveBeenCalledWith('Workflow first prod success', { + project_id: 'project123', + workflow_id: 'workflow123', + user_id: 'user123', + }); + }); + + it('should track on `first-workflow-data-loaded` event', () => { + const event: RelayEventMap['first-workflow-data-loaded'] = { + userId: 'user123', + workflowId: 'workflow123', + nodeType: 'http', + nodeId: 'node123', + credentialType: 'oAuth2', + credentialId: 'cred123', + }; + + eventService.emit('first-workflow-data-loaded', event); + + expect(telemetry.track).toHaveBeenCalledWith('Workflow first data fetched', { + user_id: 'user123', + workflow_id: 'workflow123', + node_type: 'http', + node_id: 'node123', + credential_type: 'oAuth2', + credential_id: 'cred123', + }); + }); + }); + + describe('email events', () => { + it('should track on `email-failed` event', () => { + const event: RelayEventMap['email-failed'] = { + user: { + id: 'user123', + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'global:owner', + }, + messageType: 'New user invite', + publicApi: false, + }; + + eventService.emit('email-failed', event); + + expect(telemetry.track).toHaveBeenCalledWith( + 'Instance failed to send transactional email to user', + { + user_id: 'user123', + message_type: 'New user invite', + public_api: false, + }, + ); + }); + }); +}); From c0811b218a2d02d34d281003f03db9cf2b9415c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 14 Aug 2024 11:30:26 +0200 Subject: [PATCH 03/22] refactor(core): Make new configs consistent (no-changelog) (#10393) --- .../src/configs/{cache.ts => cache.config.ts} | 0 .../{credentials.ts => credentials.config.ts} | 0 .../{database.ts => database.config.ts} | 0 .../{endpoints.ts => endpoints.config.ts} | 0 .../{event-bus.ts => event-bus.config.ts} | 0 ...-secrets.ts => external-secrets.config.ts} | 0 ...-storage.ts => external-storage.config.ts} | 0 .../src/configs/{nodes.ts => nodes.config.ts} | 0 .../{public-api.ts => public-api.config.ts} | 0 .../{templates.ts => templates.config.ts} | 0 .../{email.ts => user-management.config.ts} | 12 +++- ...ons.ts => version-notifications.config.ts} | 0 .../{workflows.ts => workflows.config.ts} | 0 packages/@n8n/config/src/index.ts | 68 +++++++++---------- 14 files changed, 40 insertions(+), 40 deletions(-) rename packages/@n8n/config/src/configs/{cache.ts => cache.config.ts} (100%) rename packages/@n8n/config/src/configs/{credentials.ts => credentials.config.ts} (100%) rename packages/@n8n/config/src/configs/{database.ts => database.config.ts} (100%) rename packages/@n8n/config/src/configs/{endpoints.ts => endpoints.config.ts} (100%) rename packages/@n8n/config/src/configs/{event-bus.ts => event-bus.config.ts} (100%) rename packages/@n8n/config/src/configs/{external-secrets.ts => external-secrets.config.ts} (100%) rename packages/@n8n/config/src/configs/{external-storage.ts => external-storage.config.ts} (100%) rename packages/@n8n/config/src/configs/{nodes.ts => nodes.config.ts} (100%) rename packages/@n8n/config/src/configs/{public-api.ts => public-api.config.ts} (100%) rename packages/@n8n/config/src/configs/{templates.ts => templates.config.ts} (100%) rename packages/@n8n/config/src/configs/{email.ts => user-management.config.ts} (92%) rename packages/@n8n/config/src/configs/{version-notifications.ts => version-notifications.config.ts} (100%) rename packages/@n8n/config/src/configs/{workflows.ts => workflows.config.ts} (100%) diff --git a/packages/@n8n/config/src/configs/cache.ts b/packages/@n8n/config/src/configs/cache.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/cache.ts rename to packages/@n8n/config/src/configs/cache.config.ts diff --git a/packages/@n8n/config/src/configs/credentials.ts b/packages/@n8n/config/src/configs/credentials.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/credentials.ts rename to packages/@n8n/config/src/configs/credentials.config.ts diff --git a/packages/@n8n/config/src/configs/database.ts b/packages/@n8n/config/src/configs/database.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/database.ts rename to packages/@n8n/config/src/configs/database.config.ts diff --git a/packages/@n8n/config/src/configs/endpoints.ts b/packages/@n8n/config/src/configs/endpoints.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/endpoints.ts rename to packages/@n8n/config/src/configs/endpoints.config.ts diff --git a/packages/@n8n/config/src/configs/event-bus.ts b/packages/@n8n/config/src/configs/event-bus.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/event-bus.ts rename to packages/@n8n/config/src/configs/event-bus.config.ts diff --git a/packages/@n8n/config/src/configs/external-secrets.ts b/packages/@n8n/config/src/configs/external-secrets.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/external-secrets.ts rename to packages/@n8n/config/src/configs/external-secrets.config.ts diff --git a/packages/@n8n/config/src/configs/external-storage.ts b/packages/@n8n/config/src/configs/external-storage.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/external-storage.ts rename to packages/@n8n/config/src/configs/external-storage.config.ts diff --git a/packages/@n8n/config/src/configs/nodes.ts b/packages/@n8n/config/src/configs/nodes.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/nodes.ts rename to packages/@n8n/config/src/configs/nodes.config.ts diff --git a/packages/@n8n/config/src/configs/public-api.ts b/packages/@n8n/config/src/configs/public-api.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/public-api.ts rename to packages/@n8n/config/src/configs/public-api.config.ts diff --git a/packages/@n8n/config/src/configs/templates.ts b/packages/@n8n/config/src/configs/templates.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/templates.ts rename to packages/@n8n/config/src/configs/templates.config.ts diff --git a/packages/@n8n/config/src/configs/email.ts b/packages/@n8n/config/src/configs/user-management.config.ts similarity index 92% rename from packages/@n8n/config/src/configs/email.ts rename to packages/@n8n/config/src/configs/user-management.config.ts index f0e130c3b48be..2c603a7148002 100644 --- a/packages/@n8n/config/src/configs/email.ts +++ b/packages/@n8n/config/src/configs/user-management.config.ts @@ -1,7 +1,7 @@ import { Config, Env, Nested } from '../decorators'; @Config -export class SmtpAuth { +class SmtpAuth { /** SMTP login username */ @Env('N8N_SMTP_USER') user = ''; @@ -20,7 +20,7 @@ export class SmtpAuth { } @Config -export class SmtpConfig { +class SmtpConfig { /** SMTP server host */ @Env('N8N_SMTP_HOST') host = ''; @@ -65,7 +65,7 @@ export class TemplateConfig { } @Config -export class EmailConfig { +class EmailConfig { /** How to send emails */ @Env('N8N_EMAIL_MODE') mode: '' | 'smtp' = 'smtp'; @@ -76,3 +76,9 @@ export class EmailConfig { @Nested template: TemplateConfig; } + +@Config +export class UserManagementConfig { + @Nested + emails: EmailConfig; +} diff --git a/packages/@n8n/config/src/configs/version-notifications.ts b/packages/@n8n/config/src/configs/version-notifications.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/version-notifications.ts rename to packages/@n8n/config/src/configs/version-notifications.config.ts diff --git a/packages/@n8n/config/src/configs/workflows.ts b/packages/@n8n/config/src/configs/workflows.config.ts similarity index 100% rename from packages/@n8n/config/src/configs/workflows.ts rename to packages/@n8n/config/src/configs/workflows.config.ts diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index 88e6fb01170d8..a5b970eab4730 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -1,85 +1,79 @@ import { Config, Env, Nested } from './decorators'; -import { CredentialsConfig } from './configs/credentials'; -import { DatabaseConfig } from './configs/database'; -import { EmailConfig } from './configs/email'; -import { VersionNotificationsConfig } from './configs/version-notifications'; -import { PublicApiConfig } from './configs/public-api'; -import { ExternalSecretsConfig } from './configs/external-secrets'; -import { TemplatesConfig } from './configs/templates'; -import { EventBusConfig } from './configs/event-bus'; -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'; +import { CredentialsConfig } from './configs/credentials.config'; +import { DatabaseConfig } from './configs/database.config'; +import { VersionNotificationsConfig } from './configs/version-notifications.config'; +import { PublicApiConfig } from './configs/public-api.config'; +import { ExternalSecretsConfig } from './configs/external-secrets.config'; +import { TemplatesConfig } from './configs/templates.config'; +import { EventBusConfig } from './configs/event-bus.config'; +import { NodesConfig } from './configs/nodes.config'; +import { ExternalStorageConfig } from './configs/external-storage.config'; +import { WorkflowsConfig } from './configs/workflows.config'; +import { EndpointsConfig } from './configs/endpoints.config'; +import { CacheConfig } from './configs/cache.config'; import { ScalingModeConfig } from './configs/scaling-mode.config'; - -@Config -class UserManagementConfig { - @Nested - emails: EmailConfig; -} +import { UserManagementConfig } from './configs/user-management.config'; @Config export class GlobalConfig { @Nested - readonly database: DatabaseConfig; + database: DatabaseConfig; @Nested - readonly credentials: CredentialsConfig; + credentials: CredentialsConfig; @Nested - readonly userManagement: UserManagementConfig; + userManagement: UserManagementConfig; @Nested - readonly versionNotifications: VersionNotificationsConfig; + versionNotifications: VersionNotificationsConfig; @Nested - readonly publicApi: PublicApiConfig; + publicApi: PublicApiConfig; @Nested - readonly externalSecrets: ExternalSecretsConfig; + externalSecrets: ExternalSecretsConfig; @Nested - readonly templates: TemplatesConfig; + templates: TemplatesConfig; @Nested - readonly eventBus: EventBusConfig; + eventBus: EventBusConfig; @Nested - readonly nodes: NodesConfig; + nodes: NodesConfig; @Nested - readonly externalStorage: ExternalStorageConfig; + externalStorage: ExternalStorageConfig; @Nested - readonly workflows: WorkflowsConfig; + workflows: WorkflowsConfig; /** Path n8n is deployed to */ @Env('N8N_PATH') - readonly path: string = '/'; + path = '/'; /** Host name n8n can be reached */ @Env('N8N_HOST') - readonly host: string = 'localhost'; + host = 'localhost'; /** HTTP port n8n can be reached */ @Env('N8N_PORT') - readonly port: number = 5678; + port = 5678; /** IP address n8n should listen on */ @Env('N8N_LISTEN_ADDRESS') - readonly listen_address: string = '0.0.0.0'; + listen_address = '0.0.0.0'; /** HTTP Protocol via which n8n can be reached */ @Env('N8N_PROTOCOL') - readonly protocol: 'http' | 'https' = 'http'; + protocol: 'http' | 'https' = 'http'; @Nested - readonly endpoints: EndpointsConfig; + endpoints: EndpointsConfig; @Nested - readonly cache: CacheConfig; + cache: CacheConfig; @Nested queue: ScalingModeConfig; From c4fcbe40c585a5dbb5ac6510618dbb0cc88bdac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Wed, 14 Aug 2024 12:52:19 +0200 Subject: [PATCH 04/22] fix(editor): Remove `@n8n/permissions` from `n8n-workflow` (no-changelog) (#10399) --- packages/editor-ui/src/Interface.ts | 4 +++- .../executions/global/GlobalExecutionsList.vue | 6 +++--- .../workflow/WorkflowExecutionsPreview.test.ts | 4 ++-- packages/editor-ui/src/stores/executions.store.ts | 9 +++++---- packages/workflow/package.json | 1 - packages/workflow/src/Interfaces.ts | 2 -- pnpm-lock.yaml | 3 --- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 986336a396d64..05c40402f1c0a 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -385,9 +385,11 @@ export interface IExecutionResponse extends IExecutionBase { executedNode?: string; } +export type ExecutionSummaryWithScopes = ExecutionSummary & { scopes: Scope[] }; + export interface IExecutionsListResponse { count: number; - results: ExecutionSummary[]; + results: ExecutionSummaryWithScopes[]; estimated: boolean; } diff --git a/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.vue b/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.vue index a0ec5726977c8..321eb4753480b 100644 --- a/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.vue +++ b/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.vue @@ -7,7 +7,7 @@ import { useToast } from '@/composables/useToast'; import { useMessage } from '@/composables/useMessage'; import { useI18n } from '@/composables/useI18n'; import { useTelemetry } from '@/composables/useTelemetry'; -import type { ExecutionFilterType, IWorkflowDb } from '@/Interface'; +import type { ExecutionFilterType, ExecutionSummaryWithScopes, IWorkflowDb } from '@/Interface'; import type { ExecutionSummary } from 'n8n-workflow'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useExecutionsStore } from '@/stores/executions.store'; @@ -16,7 +16,7 @@ import { getResourcePermissions } from '@/permissions'; const props = withDefaults( defineProps<{ - executions: ExecutionSummary[]; + executions: ExecutionSummaryWithScopes[]; filters: ExecutionFilterType; total: number; estimated: boolean; @@ -165,7 +165,7 @@ function getExecutionWorkflowName(execution: ExecutionSummary): string { } function getExecutionWorkflowPermissions( - execution: ExecutionSummary, + execution: ExecutionSummaryWithScopes, ): PermissionsRecord['workflow'] { return getResourcePermissions(execution.scopes).workflow; } diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.test.ts b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.test.ts index ce092dab46a8a..f5646e1245ad2 100644 --- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.test.ts +++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.test.ts @@ -12,7 +12,7 @@ import { i18nInstance, I18nPlugin } from '@/plugins/i18n'; import { FontAwesomePlugin } from '@/plugins/icons'; import { GlobalComponentsPlugin } from '@/plugins/components'; import { useWorkflowsStore } from '@/stores/workflows.store'; -import type { IWorkflowDb } from '@/Interface'; +import type { ExecutionSummaryWithScopes, IWorkflowDb } from '@/Interface'; let pinia: ReturnType; @@ -49,7 +49,7 @@ const generateUndefinedNullOrString = () => { } }; -const executionDataFactory = (): ExecutionSummary => ({ +const executionDataFactory = (): ExecutionSummaryWithScopes => ({ id: faker.string.uuid(), finished: faker.datatype.boolean(), mode: faker.helpers.arrayElement(['manual', 'trigger']), diff --git a/packages/editor-ui/src/stores/executions.store.ts b/packages/editor-ui/src/stores/executions.store.ts index 756fdff6c4d97..af3850fd11304 100644 --- a/packages/editor-ui/src/stores/executions.store.ts +++ b/packages/editor-ui/src/stores/executions.store.ts @@ -4,6 +4,7 @@ import type { IDataObject, ExecutionSummary } from 'n8n-workflow'; import type { ExecutionFilterType, ExecutionsQueryFilter, + ExecutionSummaryWithScopes, IExecutionDeleteFilter, IExecutionFlattedResponse, IExecutionResponse, @@ -34,7 +35,7 @@ export const useExecutionsStore = defineStore('executions', () => { const autoRefreshTimeout = ref(null); const autoRefreshDelay = ref(4 * 1000); // Refresh data every 4 secs - const executionsById = ref>({}); + const executionsById = ref>({}); const executionsCount = ref(0); const executionsCountEstimated = ref(false); const executions = computed(() => { @@ -57,7 +58,7 @@ export const useExecutionsStore = defineStore('executions', () => { }, {}), ); - const currentExecutionsById = ref>({}); + const currentExecutionsById = ref>({}); const currentExecutions = computed(() => { const data = Object.values(currentExecutionsById.value); @@ -80,14 +81,14 @@ export const useExecutionsStore = defineStore('executions', () => { const allExecutions = computed(() => [...currentExecutions.value, ...executions.value]); - function addExecution(execution: ExecutionSummary) { + function addExecution(execution: ExecutionSummaryWithScopes) { executionsById.value[execution.id] = { ...execution, mode: execution.mode, }; } - function addCurrentExecution(execution: ExecutionSummary) { + function addCurrentExecution(execution: ExecutionSummaryWithScopes) { currentExecutionsById.value[execution.id] = { ...execution, mode: execution.mode, diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 6bbdc59a1c96d..ed434fe98f831 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -39,7 +39,6 @@ "@types/xml2js": "catalog:" }, "dependencies": { - "@n8n/permissions": "workspace:*", "@n8n/tournament": "1.0.5", "@n8n_io/riot-tmpl": "4.0.0", "ast-types": "0.15.2", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 1e6e5872ffbe9..eb6c3ca07c3d4 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -10,7 +10,6 @@ import type { URLSearchParams } from 'url'; import type { RequestBodyMatcher } from 'nock'; import type { Client as SSHClient } from 'ssh2'; -import type { Scope } from '@n8n/permissions'; import type { AuthenticationMethod } from './Authentication'; import type { CODE_EXECUTION_MODES, CODE_LANGUAGES, LOG_LEVELS } from './Constants'; import type { IDeferredPromise } from './DeferredPromise'; @@ -2464,7 +2463,6 @@ export interface ExecutionSummary { nodeExecutionStatus?: { [key: string]: IExecutionSummaryNodeExecutionResult; }; - scopes?: Scope[]; } export interface IExecutionSummaryNodeExecutionResult { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16ba572ac4a4a..37fddb4bb3a1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1709,9 +1709,6 @@ importers: packages/workflow: dependencies: - '@n8n/permissions': - specifier: workspace:* - version: link:../@n8n/permissions '@n8n/tournament': specifier: 1.0.5 version: 1.0.5 From bae0116d78784dabd9c4c3143c15b241c03b1944 Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:01:14 +0100 Subject: [PATCH 05/22] fix: Remove node AI Transform node (no-changelog) (#10400) --- .../ManualChatTrigger.node.ts | 4 +- .../dynamicNodeParameters.controller.ts | 26 +- packages/cli/src/requests.ts | 6 - .../services/dynamicNodeParameters.service.ts | 27 +- packages/editor-ui/src/Interface.ts | 5 - packages/editor-ui/src/api/nodeTypes.ts | 13 - .../ButtonParameter/ButtonParameter.vue | 281 ------------------ .../src/components/ButtonParameter/utils.ts | 46 --- .../CodeNodeEditor/CodeNodeEditor.vue | 12 +- .../src/components/JsEditor/JsEditor.vue | 32 +- .../src/components/Node/NodeCreator/utils.ts | 8 - .../components/Node/NodeCreator/viewsData.ts | 11 +- .../src/components/ParameterInput.vue | 9 +- .../src/components/ParameterInputList.vue | 26 +- .../__tests__/ButtonParameter.test.ts | 136 --------- packages/editor-ui/src/constants.ts | 7 +- .../editor-ui/src/stores/nodeTypes.store.ts | 7 - .../editor-ui/src/stores/posthog.store.ts | 15 +- .../nodes/AiTransform/AiTransform.node.json | 18 -- .../nodes/AiTransform/AiTransform.node.ts | 148 --------- .../nodes/AiTransform/aitransform.svg | 10 - packages/nodes-base/package.json | 1 - packages/workflow/src/Interfaces.ts | 21 +- 23 files changed, 38 insertions(+), 831 deletions(-) delete mode 100644 packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue delete mode 100644 packages/editor-ui/src/components/ButtonParameter/utils.ts delete mode 100644 packages/editor-ui/src/components/__tests__/ButtonParameter.test.ts delete mode 100644 packages/nodes-base/nodes/AiTransform/AiTransform.node.json delete mode 100644 packages/nodes-base/nodes/AiTransform/AiTransform.node.ts delete mode 100644 packages/nodes-base/nodes/AiTransform/aitransform.svg diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.ts index 816b56b59ad40..272213d5390c2 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.ts @@ -50,9 +50,7 @@ export class ManualChatTrigger implements INodeType { name: 'openChat', type: 'button', typeOptions: { - buttonConfig: { - action: 'openChat', - }, + action: 'openChat', }, default: '', }, diff --git a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts index bfd1cf651b875..c6a46aa3d4eed 100644 --- a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts +++ b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts @@ -1,4 +1,4 @@ -import type { INodePropertyOptions, NodeParameterValueType } from 'n8n-workflow'; +import type { INodePropertyOptions } from 'n8n-workflow'; import { Post, RestController } from '@/decorators'; import { getBase } from '@/WorkflowExecuteAdditionalData'; @@ -92,28 +92,4 @@ export class DynamicNodeParametersController { credentials, ); } - - @Post('/action-result') - async getActionResult( - req: DynamicNodeParametersRequest.ActionResult, - ): Promise { - const { currentNodeParameters, nodeTypeAndVersion, path, credentials, handler, payload } = - req.body; - - const additionalData = await getBase(req.user.id, currentNodeParameters); - - if (handler) { - return await this.service.getActionResult( - handler, - path, - additionalData, - nodeTypeAndVersion, - currentNodeParameters, - payload, - credentials, - ); - } - - return; - } } diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 565020447c91d..45ee15b76326e 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -419,12 +419,6 @@ export declare namespace DynamicNodeParametersRequest { type ResourceMapperFields = BaseRequest<{ methodName: string; }>; - - /** POST /dynamic-node-parameters/action-result */ - type ActionResult = BaseRequest<{ - handler: string; - payload: IDataObject | string | undefined; - }>; } // ---------------------------------- diff --git a/packages/cli/src/services/dynamicNodeParameters.service.ts b/packages/cli/src/services/dynamicNodeParameters.service.ts index f3b0f7e192ba3..1788bac6e1877 100644 --- a/packages/cli/src/services/dynamicNodeParameters.service.ts +++ b/packages/cli/src/services/dynamicNodeParameters.service.ts @@ -15,8 +15,6 @@ import type { INodeCredentials, INodeParameters, INodeTypeNameVersion, - NodeParameterValueType, - IDataObject, } from 'n8n-workflow'; import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow'; import { NodeExecuteFunctions } from 'n8n-core'; @@ -158,24 +156,6 @@ export class DynamicNodeParametersService { return method.call(thisArgs); } - /** Returns the result of the action handler */ - async getActionResult( - handler: string, - path: string, - additionalData: IWorkflowExecuteAdditionalData, - nodeTypeAndVersion: INodeTypeNameVersion, - currentNodeParameters: INodeParameters, - payload: IDataObject | string | undefined, - credentials?: INodeCredentials, - ): Promise { - const nodeType = this.getNodeType(nodeTypeAndVersion); - const method = this.getMethod('actionHandler', handler, nodeType); - const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials); - const thisArgs = this.getThisArg(path, additionalData, workflow); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return method.call(thisArgs, payload); - } - private getMethod( type: 'resourceMapping', methodName: string, @@ -195,14 +175,9 @@ export class DynamicNodeParametersService { methodName: string, nodeType: INodeType, ): (this: ILoadOptionsFunctions) => Promise; - private getMethod( - type: 'actionHandler', - methodName: string, - nodeType: INodeType, - ): (this: ILoadOptionsFunctions, payload?: string) => Promise; private getMethod( - type: 'resourceMapping' | 'listSearch' | 'loadOptions' | 'actionHandler', + type: 'resourceMapping' | 'listSearch' | 'loadOptions', methodName: string, nodeType: INodeType, ) { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 05c40402f1c0a..04365662b28e4 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1553,11 +1553,6 @@ export declare namespace DynamicNodeParameters { interface ResourceMapperFieldsRequest extends BaseRequest { methodName: string; } - - interface ActionResultRequest extends BaseRequest { - handler: string; - payload: IDataObject | string | undefined; - } } export interface EnvironmentVariable { diff --git a/packages/editor-ui/src/api/nodeTypes.ts b/packages/editor-ui/src/api/nodeTypes.ts index f4d516aaeff82..300b25390d121 100644 --- a/packages/editor-ui/src/api/nodeTypes.ts +++ b/packages/editor-ui/src/api/nodeTypes.ts @@ -5,7 +5,6 @@ import type { INodePropertyOptions, INodeTypeDescription, INodeTypeNameVersion, - NodeParameterValueType, ResourceMapperFields, } from 'n8n-workflow'; import axios from 'axios'; @@ -58,15 +57,3 @@ export async function getResourceMapperFields( sendData, ); } - -export async function getNodeParameterActionResult( - context: IRestApiContext, - sendData: DynamicNodeParameters.ActionResultRequest, -): Promise { - return await makeRestApiRequest( - context, - 'POST', - '/dynamic-node-parameters/action-result', - sendData, - ); -} diff --git a/packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue b/packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue deleted file mode 100644 index d7e05b0f5bf85..0000000000000 --- a/packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue +++ /dev/null @@ -1,281 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/ButtonParameter/utils.ts b/packages/editor-ui/src/components/ButtonParameter/utils.ts deleted file mode 100644 index 14d3ca4d78334..0000000000000 --- a/packages/editor-ui/src/components/ButtonParameter/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Schema } from '@/Interface'; -import type { INodeExecutionData } from 'n8n-workflow'; -import { useWorkflowsStore } from '@/stores/workflows.store'; -import { useNDVStore } from '@/stores/ndv.store'; -import { useDataSchema } from '@/composables/useDataSchema'; -import { executionDataToJson } from '@/utils/nodeTypesUtils'; - -export function getParentNodes() { - const activeNode = useNDVStore().activeNode; - const { getCurrentWorkflow, getNodeByName } = useWorkflowsStore(); - const workflow = getCurrentWorkflow(); - - if (!activeNode || !workflow) return []; - - return workflow - .getParentNodesByDepth(activeNode?.name) - .filter(({ name }, i, nodes) => { - return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i; - }) - .map((n) => getNodeByName(n.name)) - .filter((n) => n !== null); -} - -export function getSchemas() { - const parentNodes = getParentNodes(); - const parentNodesNames = parentNodes.map((node) => node?.name); - const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema(); - const parentNodesSchemas: Array<{ nodeName: string; schema: Schema }> = parentNodes - .map((node) => { - const inputData: INodeExecutionData[] = getInputDataWithPinned(node); - - return { - nodeName: node?.name || '', - schema: getSchemaForExecutionData(executionDataToJson(inputData), true), - }; - }) - .filter((node) => node.schema?.value.length > 0); - - const inputSchema = parentNodesSchemas.shift(); - - return { - parentNodesNames, - inputSchema, - parentNodesSchemas, - }; -} diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index f4ba1febf2471..d59a13e98ee60 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -60,12 +60,13 @@ import jsParser from 'prettier/plugins/babel'; import * as estree from 'prettier/plugins/estree'; import { type Ref, computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; -import { CODE_NODE_TYPE } from '@/constants'; +import { ASK_AI_EXPERIMENT, CODE_NODE_TYPE } from '@/constants'; import { codeNodeEditorEventBus } from '@/event-bus'; import { useRootStore } from '@/stores/root.store'; import { usePostHog } from '@/stores/posthog.store'; import { useMessage } from '@/composables/useMessage'; +import { useSettingsStore } from '@/stores/settings.store'; import AskAI from './AskAI/AskAI.vue'; import { readOnlyEditorExtensions, writableEditorExtensions } from './baseExtensions'; import { useCompleter } from './completer'; @@ -113,6 +114,7 @@ const { autocompletionExtension } = useCompleter(() => props.mode, editor); const { createLinter } = useLinter(() => props.mode, editor); const rootStore = useRootStore(); +const settingsStore = useSettingsStore(); const posthog = usePostHog(); const i18n = useI18n(); const telemetry = useTelemetry(); @@ -189,7 +191,13 @@ onBeforeUnmount(() => { }); const aiEnabled = computed(() => { - return posthog.isAiEnabled() && props.language === 'javaScript'; + const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes( + (posthog.getVariant(ASK_AI_EXPERIMENT.name) ?? '') as string, + ); + + return ( + isAiExperimentEnabled && settingsStore.settings.ai.enabled && props.language === 'javaScript' + ); }); const placeholder = computed(() => { diff --git a/packages/editor-ui/src/components/JsEditor/JsEditor.vue b/packages/editor-ui/src/components/JsEditor/JsEditor.vue index 5a5f2cb411878..40692ab32392c 100644 --- a/packages/editor-ui/src/components/JsEditor/JsEditor.vue +++ b/packages/editor-ui/src/components/JsEditor/JsEditor.vue @@ -1,5 +1,5 @@