diff --git a/cypress/e2e/1-workflows.cy.ts b/cypress/e2e/1-workflows.cy.ts index d10f3ac850ea5..bfa5922c2a11b 100644 --- a/cypress/e2e/1-workflows.cy.ts +++ b/cypress/e2e/1-workflows.cy.ts @@ -9,7 +9,6 @@ const multipleWorkflowsCount = 5; describe('Workflows', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/10-settings-log-streaming.cy.ts b/cypress/e2e/10-settings-log-streaming.cy.ts index 0126667a699b5..10b1d4d79fe73 100644 --- a/cypress/e2e/10-settings-log-streaming.cy.ts +++ b/cypress/e2e/10-settings-log-streaming.cy.ts @@ -10,7 +10,6 @@ const settingsLogStreamingPage = new SettingsLogStreamingPage(); describe('Log Streaming Settings', () => { before(() => { - cy.resetAll(); cy.setup({ email, firstName, lastName, password }); }); diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index 8f9b90d68c322..90e9e558eb905 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -11,7 +11,6 @@ const ndv = new NDV(); describe('Undo/Redo', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index 88fea311d92cb..4d9a46bb31aff 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -4,7 +4,6 @@ const WorkflowPage = new WorkflowPageClass(); describe('Inline expression editor', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/12-canvas-actions.cy.ts b/cypress/e2e/12-canvas-actions.cy.ts index 518857106fdcb..40bd9d168e0ad 100644 --- a/cypress/e2e/12-canvas-actions.cy.ts +++ b/cypress/e2e/12-canvas-actions.cy.ts @@ -12,7 +12,6 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; const WorkflowPage = new WorkflowPageClass(); describe('Canvas Actions', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index 5f2fe8db07c37..29869e4434a52 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -22,7 +22,6 @@ const RENAME_NODE_NAME = 'Something else'; describe('Canvas Node Manipulation and Navigation', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index c278231017f9d..004e7288453f5 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -11,7 +11,6 @@ const ndv = new NDV(); describe('Data pinning', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/14-data-transformation-expressions.cy.ts b/cypress/e2e/14-data-transformation-expressions.cy.ts index cb08d51e5b1e8..43dbded37a687 100644 --- a/cypress/e2e/14-data-transformation-expressions.cy.ts +++ b/cypress/e2e/14-data-transformation-expressions.cy.ts @@ -5,7 +5,6 @@ const ndv = new NDV(); describe('Data transformation expressions', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 134deeb9fc312..b66e909b59586 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -10,7 +10,6 @@ const ndv = new NDV(); describe('Data mapping', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/15-scheduler-node.cy.ts b/cypress/e2e/15-scheduler-node.cy.ts index ae3de65c26749..b3ffd430e1631 100644 --- a/cypress/e2e/15-scheduler-node.cy.ts +++ b/cypress/e2e/15-scheduler-node.cy.ts @@ -5,11 +5,14 @@ const workflowPage = new WorkflowPage(); const ndv = new NDV(); describe('Schedule Trigger node', async () => { - beforeEach(() => { - cy.resetAll(); + before(() => { cy.skipSetup(); }); + beforeEach(() => { + workflowPage.actions.visit(); + }); + it('should execute and return the execution timestamp', () => { workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger'); workflowPage.actions.openNode('Schedule Trigger'); diff --git a/cypress/e2e/16-webhook-node.cy.ts b/cypress/e2e/16-webhook-node.cy.ts index d2bceaf22faa1..64556037d8b9c 100644 --- a/cypress/e2e/16-webhook-node.cy.ts +++ b/cypress/e2e/16-webhook-node.cy.ts @@ -92,7 +92,6 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => { describe('Webhook Trigger node', async () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index ddc028fee37f1..6cfb2260058c7 100644 --- a/cypress/e2e/17-sharing.cy.ts +++ b/cypress/e2e/17-sharing.cy.ts @@ -52,7 +52,6 @@ const users = [ describe('Sharing', () => { before(() => { - cy.resetAll(); cy.setupOwner(instanceOwner); }); diff --git a/cypress/e2e/17-workflow-tags.cy.ts b/cypress/e2e/17-workflow-tags.cy.ts index 8e14897508aea..d18c48cf7b1d7 100644 --- a/cypress/e2e/17-workflow-tags.cy.ts +++ b/cypress/e2e/17-workflow-tags.cy.ts @@ -2,51 +2,52 @@ import { WorkflowPage } from '../pages'; const wf = new WorkflowPage(); -const TEST_TAGS = ['Tag 1', 'Tag 2', 'Tag 3']; +const TEST_TAGS = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4', 'Tag 5']; describe('Workflow tags', () => { - beforeEach(() => { - cy.resetAll(); + before(() => { cy.skipSetup(); }); + beforeEach(() => { + wf.actions.visit(); + }); + it('should create and attach tags inline', () => { wf.getters.createTagButton().click(); - wf.actions.addTags(TEST_TAGS); - wf.getters.tagPills().should('have.length', TEST_TAGS.length); + wf.actions.addTags(TEST_TAGS.slice(0, 2)); + wf.getters.tagPills().should('have.length', 2); wf.getters.nthTagPill(1).click(); - wf.actions.addTags('Tag 4'); - wf.getters.tagPills().should('have.length', TEST_TAGS.length + 1); + wf.actions.addTags(TEST_TAGS[2]); + wf.getters.tagPills().should('have.length', 3); wf.getters.isWorkflowSaved(); }); it('should create tags via modal', () => { wf.actions.openTagManagerModal(); - const [first, second] = TEST_TAGS; - - cy.contains('Create a tag').click(); - cy.getByTestId('tags-table').find('input').type(first).type('{enter}'); - cy.contains('Add new').click(); - cy.wait(300); - cy.getByTestId('tags-table').find('input').type(second).type('{enter}'); + const tags = TEST_TAGS.slice(3); + for (const tag of tags) { + cy.contains('Add new').click(); + cy.getByTestId('tags-table').find('input').type(tag).type('{enter}'); + cy.wait(300); + } cy.contains('Done').click(); wf.getters.createTagButton().click(); - wf.getters.tagsInDropdown().should('have.length', 2); // two stored + wf.getters.tagsInDropdown().should('have.length', 5); wf.getters.tagPills().should('have.length', 0); // none attached }); - it('should delete a tag via modal', () => { + it('should delete all tags via modal', () => { wf.actions.openTagManagerModal(); - const [first] = TEST_TAGS; + TEST_TAGS.forEach(() => { + cy.getByTestId('delete-tag-button').first().click({ force: true }); + cy.contains('Delete tag').click(); + cy.wait(300); + }); - cy.contains('Create a tag').click(); - cy.getByTestId('tags-table').find('input').type(first).type('{enter}'); - cy.getByTestId('delete-tag-button').click({ force: true }); - cy.wait(300); - cy.contains('Delete tag').click(); cy.contains('Done').click(); wf.getters.createTagButton().click(); wf.getters.tagsInDropdown().should('have.length', 0); // none stored diff --git a/cypress/e2e/18-user-management.cy.ts b/cypress/e2e/18-user-management.cy.ts index f06ba3d6559d9..9f51f561d97a2 100644 --- a/cypress/e2e/18-user-management.cy.ts +++ b/cypress/e2e/18-user-management.cy.ts @@ -51,7 +51,6 @@ const personalSettingsPage = new PersonalSettingsPage(); describe('User Management', () => { before(() => { - cy.resetAll(); cy.setupOwner(instanceOwner); }); diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 6ad2ce6592fa1..983e5e4bba425 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -6,11 +6,14 @@ const workflowPage = new WorkflowPageClass(); const ndv = new NDV(); describe('Execution', () => { - beforeEach(() => { - cy.resetAll(); + before(() => { cy.skipSetup(); }); + beforeEach(() => { + workflowPage.actions.visit(); + }); + it('should test manual workflow', () => { cy.createFixtureWorkflow('Manual_wait_set.json', `Manual wait set ${uuid()}`); @@ -264,7 +267,6 @@ describe('Execution', () => { .canvasNodeByName('Set') .within(() => cy.get('.fa-check').should('not.exist')); - // Check canvas nodes after workflow stopped workflowPage.getters .canvasNodeByName('Webhook') diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 17d51ebb0ded4..d8f3fc5e0e50e 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -33,7 +33,6 @@ const NEW_CREDENTIAL_NAME = 'Something else'; describe('Credentials', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/20-workflow-executions.cy.ts b/cypress/e2e/20-workflow-executions.cy.ts index 1e40b7dcc4f8e..fe40633c38d2d 100644 --- a/cypress/e2e/20-workflow-executions.cy.ts +++ b/cypress/e2e/20-workflow-executions.cy.ts @@ -7,7 +7,6 @@ const executionsTab = new WorkflowExecutionsTab(); // Test suite for executions tab describe('Current Workflow Executions', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/21-community-nodes.cy.ts b/cypress/e2e/21-community-nodes.cy.ts index f46a829ac033f..d48d365c4f6bf 100644 --- a/cypress/e2e/21-community-nodes.cy.ts +++ b/cypress/e2e/21-community-nodes.cy.ts @@ -14,7 +14,6 @@ const workflowPage = new WorkflowPage(); // We want to keep the other tests as fast as possible so we don't want to break the cache in those. describe('Community Nodes', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }) beforeEach(() => { diff --git a/cypress/e2e/22-user-activation-modal.cy.ts b/cypress/e2e/22-user-activation-modal.cy.ts index eb598abff60c6..e9a730623d25e 100644 --- a/cypress/e2e/22-user-activation-modal.cy.ts +++ b/cypress/e2e/22-user-activation-modal.cy.ts @@ -9,11 +9,11 @@ const userActivationSurveyModal = new UserActivationSurveyModal(); const BASE_WEBHOOK_URL = 'http://localhost:5678/webhook'; describe('User activation survey', () => { - it('Should show activation survey', () => { - cy.resetAll(); - + before(() => { cy.skipSetup(); + }); + it('Should show activation survey', () => { cy.intercept('GET', '/rest/settings', (req) => { req.reply(SettingsWithActivationModalEnabled); }); diff --git a/cypress/e2e/23-variables.cy.ts b/cypress/e2e/23-variables.cy.ts index ce78f8fbe3db5..90ccfedae24fd 100644 --- a/cypress/e2e/23-variables.cy.ts +++ b/cypress/e2e/23-variables.cy.ts @@ -11,7 +11,6 @@ const lastName = randLastName(); describe('Variables', () => { before(() => { - cy.resetAll(); cy.setup({ email, firstName, lastName, password }); }); diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts index 05f5dd8581a71..b7f6c0f437db1 100644 --- a/cypress/e2e/24-ndv-paired-item.cy.ts +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -6,10 +6,9 @@ const ndv = new NDV(); describe('NDV', () => { before(() => { - cy.resetAll(); cy.skipSetup(); - }); + beforeEach(() => { workflowPage.actions.visit(); workflowPage.actions.renameWorkflow(uuid()); diff --git a/cypress/e2e/25-stickies.cy.ts b/cypress/e2e/25-stickies.cy.ts index 0746fddc0326a..13396efe25117 100644 --- a/cypress/e2e/25-stickies.cy.ts +++ b/cypress/e2e/25-stickies.cy.ts @@ -15,9 +15,11 @@ function checkStickiesStyle( top: number, left: number, height: number, width: n } describe('Canvas Actions', () => { - beforeEach(() => { - cy.resetAll(); + before(() => { cy.skipSetup(); + }); + + beforeEach(() => { workflowPage.actions.visit(); cy.window().then( diff --git a/cypress/e2e/26-resource-locator.cy.ts b/cypress/e2e/26-resource-locator.cy.ts index e0ba34d70aa20..3a00dded78ea3 100644 --- a/cypress/e2e/26-resource-locator.cy.ts +++ b/cypress/e2e/26-resource-locator.cy.ts @@ -9,7 +9,6 @@ const INVALID_CREDENTIALS_MESSAGE = 'Please check your credential'; describe('Resource Locator', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/3-default-owner.cy.ts b/cypress/e2e/3-default-owner.cy.ts index 6aba65180c5b4..bd9b29f0366f9 100644 --- a/cypress/e2e/3-default-owner.cy.ts +++ b/cypress/e2e/3-default-owner.cy.ts @@ -35,7 +35,6 @@ const lastName = randLastName(); describe('Default owner', () => { it('should be able to create workflows', () => { - cy.resetAll(); cy.skipSetup(); cy.createFixtureWorkflow('Test_workflow_1.json', `Test workflow`); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 0ad0306cb6b93..187345e940079 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -6,10 +6,8 @@ const nodeCreatorFeature = new NodeCreator(); const WorkflowPage = new WorkflowPageClass(); const NDVModal = new NDV(); - describe('Node Creator', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 8c8809ad26c3e..a375330cf70fc 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -6,10 +6,9 @@ const ndv = new NDV(); describe('NDV', () => { before(() => { - cy.resetAll(); cy.skipSetup(); - }); + beforeEach(() => { workflowPage.actions.visit(); workflowPage.actions.renameWorkflow(uuid()); diff --git a/cypress/e2e/6-code-node.cy.ts b/cypress/e2e/6-code-node.cy.ts index 7fa465d2ee86d..9a12dfccf4591 100644 --- a/cypress/e2e/6-code-node.cy.ts +++ b/cypress/e2e/6-code-node.cy.ts @@ -6,7 +6,6 @@ const ndv = new NDV(); describe('Code node', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index baa38e9f7a205..b7f948744ca88 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -15,7 +15,6 @@ const WorkflowPage = new WorkflowPageClass(); describe('Workflow Actions', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); @@ -111,8 +110,6 @@ describe('Workflow Actions', () => { }); it('should update workflow settings', () => { - cy.resetAll(); - cy.skipSetup(); WorkflowPage.actions.visit(); // Open settings dialog WorkflowPage.actions.saveWorkflowOnButtonClick(); @@ -121,7 +118,7 @@ describe('Workflow Actions', () => { WorkflowPage.getters.workflowMenuItemSettings().should('be.visible'); WorkflowPage.getters.workflowMenuItemSettings().click(); // Change all settings - WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', 2); + WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', 7); WorkflowPage.getters .workflowSettingsErrorWorkflowSelect() .find('li') @@ -229,6 +226,7 @@ describe('Workflow Actions', () => { it('should duplicate unsaved workflow', () => { duplicateWorkflow(); }); + it('should duplicate saved workflow', () => { WorkflowPage.actions.saveWorkflowOnButtonClick(); duplicateWorkflow(); diff --git a/cypress/e2e/8-http-request-node.cy.ts b/cypress/e2e/8-http-request-node.cy.ts index 9c3cce11e85c3..a40d37cf23248 100644 --- a/cypress/e2e/8-http-request-node.cy.ts +++ b/cypress/e2e/8-http-request-node.cy.ts @@ -5,7 +5,6 @@ const ndv = new NDV(); describe('HTTP Request node', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index dd4e01128b134..6b0412cc21a7f 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -4,7 +4,6 @@ const WorkflowPage = new WorkflowPageClass(); describe('Expression editor modal', () => { before(() => { - cy.resetAll(); cy.skipSetup(); }); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index ee534b4dc59fd..1df0199296c1d 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -15,6 +15,10 @@ import './commands'; +before(() => { + cy.resetAll(); +}); + // Load custom nodes and credentials fixtures beforeEach(() => { cy.intercept('GET', '/rest/settings').as('loadSettings'); @@ -25,6 +29,6 @@ beforeEach(() => { statusCode: 200, body: { data: { status: 'success', message: 'Tested successfully' }, - } + }, }); -}) +}); diff --git a/jest.config.js b/jest.config.js index 729d2d3a59d5a..e6eb3e16c85a8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,6 +26,7 @@ const config = { collectCoverage: true, coverageReporters: [process.env.COVERAGE_REPORT === 'true' ? 'text' : 'text-summary'], collectCoverageFrom: ['src/**/*.ts'], + testTimeout: 10_000, }; if (process.env.CI === 'true') { diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index ad3b041f691c7..9d36930f5efce 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -21,6 +21,7 @@ import type { ExecutionStatus, IExecutionsSummary, FeatureFlags, + IUserSettings, } from 'n8n-workflow'; import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; @@ -478,13 +479,6 @@ export interface IPersonalizationSurveyAnswers { workArea: string[] | string | null; } -export interface IUserSettings { - isOnboarded?: boolean; - showUserActivationSurvey?: boolean; - firstSuccessfulWorkflowId?: string; - userActivated?: boolean; -} - export interface IActiveDirectorySettings { enabled: boolean; } diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 55793c0528d77..4656658c1da51 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -31,6 +31,7 @@ import type { User } from '@db/entities/User'; import { N8N_VERSION } from '@/constants'; import * as Db from '@/Db'; import { NodeTypes } from './NodeTypes'; +import type { ExecutionMetadata } from './databases/entities/ExecutionMetadata'; function userToPayload(user: User): { userId: string; @@ -253,7 +254,17 @@ export class InternalHooks implements IInternalHooksClass { executionId: string, executionMode: WorkflowExecuteMode, workflowData?: IWorkflowBase, + executionMetadata?: ExecutionMetadata[], ): Promise { + let metaData; + try { + if (executionMetadata) { + metaData = executionMetadata.reduce((acc, meta) => { + return { ...acc, [meta.key]: meta.value }; + }, {}); + } + } catch {} + void Promise.all([ eventBus.sendWorkflowEvent({ eventName: 'n8n.workflow.crashed', @@ -262,6 +273,7 @@ export class InternalHooks implements IInternalHooksClass { isManual: executionMode === 'manual', workflowId: workflowData?.id?.toString(), workflowName: workflowData?.name, + metaData, }, }), ]); @@ -430,6 +442,7 @@ export class InternalHooks implements IInternalHooksClass { workflowId: properties.workflow_id, isManual: properties.is_manual, workflowName: workflow.name, + metaData: runData?.data?.resultData?.metadata, }, }) : eventBus.sendWorkflowEvent({ @@ -445,6 +458,7 @@ export class InternalHooks implements IInternalHooksClass { errorMessage: properties.error_message?.toString(), isManual: properties.is_manual, workflowName: workflow.name, + metaData: runData?.data?.resultData?.metadata, }, }), ); diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index 4f8b121bbb041..c6a40455fea63 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -1,3 +1,4 @@ +import type { FindOptionsWhere } from 'typeorm'; import { In } from 'typeorm'; import { Container } from 'typedi'; import type { @@ -26,16 +27,16 @@ import type { } from '@/Interfaces'; import { NodeTypes } from '@/NodeTypes'; import { WorkflowRunner } from '@/WorkflowRunner'; - import config from '@/config'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { User } from '@db/entities/User'; import { RoleRepository } from '@db/repositories'; -import { whereClause } from '@/UserManagement/UserManagementHelper'; import omit from 'lodash.omit'; import { PermissionChecker } from './UserManagement/PermissionChecker'; import { isWorkflowIdValid } from './utils'; import { UserService } from './user/user.service'; +import type { SharedWorkflow } from './databases/entities/SharedWorkflow'; +import type { RoleNames } from './databases/entities/Role'; const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); @@ -372,16 +373,24 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi * Get the IDs of the workflows that have been shared with the user. * Returns all IDs if user is global owner (see `whereClause`) */ -export async function getSharedWorkflowIds(user: User, roles?: string[]): Promise { +export async function getSharedWorkflowIds(user: User, roles?: RoleNames[]): Promise { + const where: FindOptionsWhere = {}; + if (user.globalRole?.name !== 'owner') { + where.userId = user.id; + } + if (roles?.length) { + const roleIds = await Db.collections.Role.find({ + select: ['id'], + where: { name: In(roles), scope: 'workflow' }, + }).then((data) => data.map(({ id }) => id)); + where.roleId = In(roleIds); + } const sharedWorkflows = await Db.collections.SharedWorkflow.find({ - relations: ['workflow', 'role'], - where: whereClause({ user, entityType: 'workflow', roles }), + where, select: ['workflowId'], }); - return sharedWorkflows.map(({ workflowId }) => workflowId); } - /** * Check if user owns more than 15 workflows or more than 2 workflows with at least 2 nodes. * If user does, set flag in its settings. diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 58390f4959606..c2ea34e908806 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -133,6 +133,7 @@ export class WorkflowRunner { executionId, executionMode, executionFlattedData?.workflowData, + executionFlattedData?.metadata, ); } catch { // Ignore errors diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 6f20ba3fb9e0f..e75db78b7aa11 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -1,16 +1,18 @@ import convict from 'convict'; import dotenv from 'dotenv'; import { tmpdir } from 'os'; -import { mkdtempSync, readFileSync } from 'fs'; +import { mkdirSync, mkdtempSync, readFileSync } from 'fs'; import { join } from 'path'; import { schema } from './schema'; import { inTest, inE2ETests } from '@/constants'; if (inE2ETests) { + const testsDir = join(tmpdir(), 'n8n-e2e/'); + mkdirSync(testsDir, { recursive: true }); // Skip loading config from env variables in end-to-end tests process.env = { E2E_TESTS: 'true', - N8N_USER_FOLDER: mkdtempSync(join(tmpdir(), 'n8n-e2e-')), + N8N_USER_FOLDER: mkdtempSync(testsDir), EXECUTIONS_PROCESS: 'main', N8N_DIAGNOSTICS_ENABLED: 'false', N8N_PUBLIC_API_DISABLED: 'true', @@ -19,8 +21,12 @@ if (inE2ETests) { NODE_FUNCTION_ALLOW_EXTERNAL: 'node-fetch', }; } else if (inTest) { + const testsDir = join(tmpdir(), 'n8n-tests/'); + mkdirSync(testsDir, { recursive: true }); + process.env.N8N_LOG_LEVEL = 'silent'; + process.env.N8N_ENCRYPTION_KEY = 'test-encryption-key'; process.env.N8N_PUBLIC_API_DISABLED = 'true'; - process.env.N8N_PUBLIC_API_SWAGGERUI_DISABLED = 'true'; + process.env.N8N_USER_FOLDER = mkdtempSync(testsDir); } else { dotenv.config(); } diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index f025b60eea0b2..5ae9fe0d7a894 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -76,7 +76,10 @@ export class AuthController { // attempt to fetch user data with the credentials, but don't log in yet const preliminaryUser = await handleEmailLogin(email, password); // if the user is an owner, continue with the login - if (preliminaryUser?.globalRole?.name === 'owner') { + if ( + preliminaryUser?.globalRole?.name === 'owner' || + preliminaryUser?.settings?.allowSSOManualLogin + ) { user = preliminaryUser; usedAuthenticationMethod = 'email'; } else { diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 64e46582066bd..f316fe2da9325 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -1,5 +1,4 @@ import { IsNull, MoreThanOrEqual, Not } from 'typeorm'; -import { v4 as uuid } from 'uuid'; import validator from 'validator'; import { Get, Post, RestController } from '@/decorators'; import { @@ -25,6 +24,7 @@ import type { IDatabaseCollections, IExternalHooksClass, IInternalHooksClass } f import { issueCookie } from '@/auth/jwt'; import { isLdapEnabled } from '@/Ldap/helpers'; import { isSamlCurrentAuthenticationMethod } from '../sso/ssoHelpers'; +import { UserService } from '../user/user.service'; @RestController() export class PasswordResetController { @@ -103,7 +103,10 @@ export class PasswordResetController { relations: ['authIdentities', 'globalRole'], }); - if (isSamlCurrentAuthenticationMethod() && user?.globalRole.name !== 'owner') { + if ( + isSamlCurrentAuthenticationMethod() && + !(user?.globalRole.name === 'owner' || user?.settings?.allowSSOManualLogin === true) + ) { this.logger.debug( 'Request to send password reset email failed because login is handled by SAML', ); @@ -126,18 +129,9 @@ export class PasswordResetController { throw new UnprocessableRequestError('forgotPassword.ldapUserPasswordResetUnavailable'); } - user.resetPasswordToken = uuid(); - - const { id, firstName, lastName, resetPasswordToken } = user; - - const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 7200; - - await this.userRepository.update(id, { resetPasswordToken, resetPasswordTokenExpiration }); - const baseUrl = getInstanceBaseUrl(); - const url = new URL(`${baseUrl}/change-password`); - url.searchParams.append('userId', id); - url.searchParams.append('token', resetPasswordToken); + const { id, firstName, lastName } = user; + const url = UserService.generatePasswordResetUrl(user); try { await this.mailer.passwordReset({ diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index c2ae07d344d1d..e68f8699685f0 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -5,7 +5,7 @@ import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { User } from '@db/entities/User'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; -import { Authorized, NoAuthRequired, Delete, Get, Post, RestController } from '@/decorators'; +import { Authorized, NoAuthRequired, Delete, Get, Post, RestController, Patch } from '@/decorators'; import { addInviteLinkToUser, generateUserInviteUrl, @@ -20,7 +20,7 @@ import { issueCookie } from '@/auth/jwt'; import { BadRequestError, InternalServerError, NotFoundError } from '@/ResponseHelper'; import { Response } from 'express'; import type { Config } from '@/config'; -import { UserRequest } from '@/requests'; +import { UserRequest, UserSettingsUpdatePayload } from '@/requests'; import type { UserManagementMailer } from '@/UserManagement/email'; import type { PublicUser, @@ -40,6 +40,8 @@ import type { SharedWorkflowRepository, UserRepository, } from '@db/repositories'; +import { UserService } from '../user/user.service'; +import { plainToInstance } from 'class-transformer'; @Authorized(['global', 'owner']) @RestController('/users') @@ -355,6 +357,38 @@ export class UsersController { ); } + @Authorized(['global', 'owner']) + @Get('/:id/password-reset-link') + async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) { + const user = await this.userRepository.findOneOrFail({ + where: { id: req.params.id }, + }); + if (!user) { + throw new NotFoundError('User not found'); + } + const link = await UserService.generatePasswordResetUrl(user); + return { + link, + }; + } + + @Authorized(['global', 'owner']) + @Patch('/:id/settings') + async updateUserSettings(req: UserRequest.UserSettingsUpdate) { + const payload = plainToInstance(UserSettingsUpdatePayload, req.body); + + const id = req.params.id; + + await UserService.updateUserSettings(id, payload); + + const user = await this.userRepository.findOneOrFail({ + select: ['settings'], + where: { id }, + }); + + return user.settings; + } + /** * Delete a user. Optionally, designate a transferee for their workflows and credentials. */ diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index 6cba438f7942a..1aa334d489bf3 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -11,14 +11,14 @@ import { BeforeInsert, } from 'typeorm'; import { IsEmail, IsString, Length } from 'class-validator'; -import type { IUser } from 'n8n-workflow'; +import type { IUser, IUserSettings } from 'n8n-workflow'; import { Role } from './Role'; import type { SharedWorkflow } from './SharedWorkflow'; import type { SharedCredentials } from './SharedCredentials'; import { NoXss } from '../utils/customValidators'; import { objectRetriever, lowerCaser } from '../utils/transformers'; import { AbstractEntity, jsonColumnType } from './AbstractEntity'; -import type { IPersonalizationSurveyAnswers, IUserSettings } from '@/Interfaces'; +import type { IPersonalizationSurveyAnswers } from '@/Interfaces'; import type { AuthIdentity } from './AuthIdentity'; export const MIN_PASSWORD_LENGTH = 8; diff --git a/packages/cli/src/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee.ts b/packages/cli/src/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee.ts index daf63c39a8f39..8eb9e0a75a944 100644 --- a/packages/cli/src/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee.ts +++ b/packages/cli/src/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee.ts @@ -359,7 +359,11 @@ export class MessageEventBusDestinationWebhook } } } catch (error) { - console.error(error); + LoggerProxy.warn( + `Webhook destination ${this.label} failed to send message to: ${this.url} - ${ + (error as Error).message + }`, + ); } return sendResult; diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index af1fdae591b43..a18e8b0f26f0d 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -40,6 +40,10 @@ export class UserSettingsUpdatePayload { @IsBoolean({ message: 'userActivated should be a boolean' }) @IsOptional() userActivated: boolean; + + @IsBoolean({ message: 'allowSSOManualLogin should be a boolean' }) + @IsOptional() + allowSSOManualLogin?: boolean; } export type AuthlessRequest< @@ -250,6 +254,14 @@ export declare namespace UserRequest { { limit?: number; offset?: number; cursor?: string; includeRole?: boolean } >; + export type PasswordResetLink = AuthenticatedRequest<{ id: string }, {}, {}, {}>; + + export type UserSettingsUpdate = AuthenticatedRequest< + { id: string }, + {}, + UserSettingsUpdatePayload + >; + export type Reinvite = AuthenticatedRequest<{ id: string }>; export type Update = AuthlessRequest< diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts index 7edb6f6172f41..f278bac24fc83 100644 --- a/packages/cli/src/telemetry/index.ts +++ b/packages/cli/src/telemetry/index.ts @@ -79,19 +79,29 @@ export class Telemetry { return; } - const allPromises = Object.keys(this.executionCountsBuffer).map(async (workflowId) => { - const promise = this.track( - 'Workflow execution count', - { - event_version: '2', - workflow_id: workflowId, - ...this.executionCountsBuffer[workflowId], - }, - { withPostHog: true }, - ); - - return promise; - }); + const allPromises = Object.keys(this.executionCountsBuffer) + .filter((workflowId) => { + const data = this.executionCountsBuffer[workflowId]; + const sum = + (data.manual_error?.count ?? 0) + + (data.manual_success?.count ?? 0) + + (data.prod_error?.count ?? 0) + + (data.prod_success?.count ?? 0); + return sum > 0; + }) + .map(async (workflowId) => { + const promise = this.track( + 'Workflow execution count', + { + event_version: '2', + workflow_id: workflowId, + ...this.executionCountsBuffer[workflowId], + }, + { withPostHog: true }, + ); + + return promise; + }); this.executionCountsBuffer = {}; @@ -128,7 +138,11 @@ export class Telemetry { this.executionCountsBuffer[workflowId][key]!.count++; } - if (!properties.success && properties.error_node_type?.startsWith('n8n-nodes-base')) { + if ( + !properties.success && + properties.is_manual && + properties.error_node_type?.startsWith('n8n-nodes-base') + ) { void this.track('Workflow execution errored', properties); } } diff --git a/packages/cli/src/user/user.service.ts b/packages/cli/src/user/user.service.ts index a7f8bff52d0c8..6183a97e59d59 100644 --- a/packages/cli/src/user/user.service.ts +++ b/packages/cli/src/user/user.service.ts @@ -1,8 +1,10 @@ import type { EntityManager, FindOptionsWhere } from 'typeorm'; import { In } from 'typeorm'; +import { v4 as uuid } from 'uuid'; import * as Db from '@/Db'; import { User } from '@db/entities/User'; -import type { IUserSettings } from '@/Interfaces'; +import type { IUserSettings } from 'n8n-workflow'; +import { getInstanceBaseUrl } from '../UserManagement/UserManagementHelper'; export class UserService { static async get(where: FindOptionsWhere): Promise { @@ -22,4 +24,17 @@ export class UserService { }); return Db.collections.User.update(id, { settings: { ...currentSettings, ...userSettings } }); } + + static async generatePasswordResetUrl(user: User): Promise { + user.resetPasswordToken = uuid(); + const { id, resetPasswordToken } = user; + const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 7200; + await Db.collections.User.update(id, { resetPasswordToken, resetPasswordTokenExpiration }); + + const baseUrl = getInstanceBaseUrl(); + const url = new URL(`${baseUrl}/change-password`); + url.searchParams.append('userId', id); + url.searchParams.append('token', resetPasswordToken); + return url.toString(); + } } diff --git a/packages/cli/src/workflows/workflows.services.ts b/packages/cli/src/workflows/workflows.services.ts index e942620e070a9..747cede32ca07 100644 --- a/packages/cli/src/workflows/workflows.services.ts +++ b/packages/cli/src/workflows/workflows.services.ts @@ -27,6 +27,7 @@ import { getSharedWorkflowIds } from '@/WorkflowHelpers'; import { isSharingEnabled, whereClause } from '@/UserManagement/UserManagementHelper'; import type { WorkflowForList } from '@/workflows/workflows.types'; import { InternalHooks } from '@/InternalHooks'; +import type { RoleNames } from '../databases/entities/Role'; export type IGetWorkflowsQueryFilter = Pick< FindOptionsWhere, @@ -111,7 +112,7 @@ export class WorkflowsService { } // Warning: this function is overridden by EE to disregard role list. - static async getWorkflowIdsForUser(user: User, roles?: string[]): Promise { + static async getWorkflowIdsForUser(user: User, roles?: RoleNames[]): Promise { return getSharedWorkflowIds(user, roles); } diff --git a/packages/cli/test/integration/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows.controller.ee.test.ts index bf8ff9948f2a3..223f0bcc521b6 100644 --- a/packages/cli/test/integration/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows.controller.ee.test.ts @@ -8,11 +8,12 @@ import type { User } from '@db/entities/User'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; -import { createWorkflow } from './shared/testDb'; +import { createWorkflow, getGlobalMemberRole, getGlobalOwnerRole } from './shared/testDb'; import type { SaveCredentialFunction } from './shared/types'; import { makeWorkflow } from './shared/utils'; import { randomCredentialPayload } from './shared/random'; import { License } from '@/License'; +import { getSharedWorkflowIds } from '../../src/WorkflowHelpers'; let owner: User; let member: User; @@ -850,3 +851,28 @@ describe('PATCH /workflows/:id - validate interim updates', () => { expect(updateAttemptResponse.body.code).toBe(100); }); }); + +describe('getSharedWorkflowIds', () => { + it('should show all workflows to owners', async () => { + owner.globalRole = await getGlobalOwnerRole(); + const workflow1 = await createWorkflow({}, member); + const workflow2 = await createWorkflow({}, anotherMember); + const sharedWorkflowIds = await getSharedWorkflowIds(owner); + expect(sharedWorkflowIds).toHaveLength(2); + expect(sharedWorkflowIds).toContain(workflow1.id); + expect(sharedWorkflowIds).toContain(workflow2.id); + }); + + it('should show shared workflows to users', async () => { + member.globalRole = await getGlobalMemberRole(); + const workflow1 = await createWorkflow({}, anotherMember); + const workflow2 = await createWorkflow({}, anotherMember); + const workflow3 = await createWorkflow({}, anotherMember); + await testDb.shareWorkflowWithUsers(workflow1, [member]); + await testDb.shareWorkflowWithUsers(workflow3, [member]); + const sharedWorkflowIds = await getSharedWorkflowIds(member); + expect(sharedWorkflowIds).toHaveLength(2); + expect(sharedWorkflowIds).toContain(workflow1.id); + expect(sharedWorkflowIds).toContain(workflow3.id); + }); +}); diff --git a/packages/cli/test/unit/Telemetry.test.ts b/packages/cli/test/unit/Telemetry.test.ts index 988571e51bd3e..4df728add1f2f 100644 --- a/packages/cli/test/unit/Telemetry.test.ts +++ b/packages/cli/test/unit/Telemetry.test.ts @@ -211,7 +211,6 @@ describe('Telemetry', () => { await telemetry.trackWorkflowExecution(payload); expect(spyTrack).toHaveBeenCalledTimes(0); - execBuffer = telemetry.getCountsBuffer(); expect(execBuffer['1'].manual_error).toBeUndefined(); @@ -254,19 +253,20 @@ describe('Telemetry', () => { // failed execution n8n node payload.success = false; payload.error_node_type = 'n8n-nodes-base.merge'; + payload.is_manual = true; await telemetry.trackWorkflowExecution(payload); expect(spyTrack).toHaveBeenCalledTimes(1); execBuffer = telemetry.getCountsBuffer(); - expect(execBuffer['1'].manual_error).toBeUndefined(); + expect(execBuffer['1'].manual_error?.count).toBe(1); expect(execBuffer['1'].manual_success).toBeUndefined(); expect(execBuffer['2'].manual_error).toBeUndefined(); expect(execBuffer['2'].manual_success).toBeUndefined(); expect(execBuffer['2'].prod_error).toBeUndefined(); expect(execBuffer['1'].prod_success?.count).toBe(2); - expect(execBuffer['1'].prod_error?.count).toBe(2); + expect(execBuffer['1'].prod_error?.count).toBe(1); expect(execBuffer['2'].prod_success?.count).toBe(2); expect(execBuffer['1'].prod_error?.first).toEqual(execTime2); diff --git a/packages/design-system/src/components/N8nUserInfo/UserInfo.vue b/packages/design-system/src/components/N8nUserInfo/UserInfo.vue index 248507b052aec..ec780a157752e 100644 --- a/packages/design-system/src/components/N8nUserInfo/UserInfo.vue +++ b/packages/design-system/src/components/N8nUserInfo/UserInfo.vue @@ -23,7 +23,14 @@
- Sign-in type: {{ signInType }} + Sign-in type: + {{ + isSamlLoginEnabled + ? settings?.allowSSOManualLogin + ? $locale.baseText('settings.sso') + ' + ' + signInType + : $locale.baseText('settings.sso') + : signInType + }}
@@ -71,6 +78,14 @@ export default defineComponent({ type: String, required: false, }, + settings: { + type: Object, + required: false, + }, + isSamlLoginEnabled: { + type: Boolean, + required: false, + }, }, computed: { classes(): Record { diff --git a/packages/design-system/src/components/N8nUsersList/UsersList.vue b/packages/design-system/src/components/N8nUsersList/UsersList.vue index 9ea1075c894fd..9ab256dd412f2 100644 --- a/packages/design-system/src/components/N8nUsersList/UsersList.vue +++ b/packages/design-system/src/components/N8nUsersList/UsersList.vue @@ -7,7 +7,11 @@ :class="i === sortedUsers.length - 1 ? $style.itemContainer : $style.itemWithBorder" :data-test-id="`user-list-item-${user.email}`" > - +
{{ t('nds.auth.roles.owner') }} @@ -67,6 +71,10 @@ export default defineComponent({ type: Array as PropType, default: () => [], }, + isSamlLoginEnabled: { + type: Boolean, + default: false, + }, }, computed: { sortedUsers(): IUser[] { diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index cbd8757126907..fdb084f1fa7ba 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -193,7 +193,7 @@ export default defineComponent({ }, async checkForCloudPlanData(): Promise { try { - await this.cloudPlanStore.getOwnerCurrentPLan(); + await this.cloudPlanStore.getOwnerCurrentPlan(); if (!this.cloudPlanStore.userIsTrialing) return; await this.cloudPlanStore.getInstanceCurrentUsage(); this.startPollingInstanceUsageData(); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 3b9546127abcd..8e561d56646c7 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -33,6 +33,7 @@ import type { IN8nUISettings, IUserManagementSettings, WorkflowSettings, + IUserSettings, } from 'n8n-workflow'; import type { SignInType } from './constants'; import type { @@ -561,12 +562,7 @@ export interface IUserResponse { personalizationAnswers?: IPersonalizationSurveyVersions | null; isPending: boolean; signInType?: SignInType; - settings?: { - isOnboarded?: boolean; - showUserActivationSurvey?: boolean; - firstSuccessfulWorkflowId?: string; - userActivated?: boolean; - }; + settings?: IUserSettings; } export interface CurrentUserResponse extends IUserResponse { diff --git a/packages/editor-ui/src/api/users.ts b/packages/editor-ui/src/api/users.ts index 5067da18c4bf2..bb360d4902763 100644 --- a/packages/editor-ui/src/api/users.ts +++ b/packages/editor-ui/src/api/users.ts @@ -105,7 +105,15 @@ export async function updateCurrentUserSettings( context: IRestApiContext, settings: IUserResponse['settings'], ): Promise { - return makeRestApiRequest(context, 'PATCH', '/me/settings', settings); + return makeRestApiRequest(context, 'PATCH', '/me/settings', settings as IDataObject); +} + +export async function updateOtherUserSettings( + context: IRestApiContext, + userId: string, + settings: IUserResponse['settings'], +): Promise { + return makeRestApiRequest(context, 'PATCH', `/users/${userId}/settings`, settings as IDataObject); } export async function updateCurrentUserPassword( @@ -144,6 +152,13 @@ export async function getInviteLink( return makeRestApiRequest(context, 'GET', `/users/${id}/invite-link`); } +export async function getPasswordResetLink( + context: IRestApiContext, + { id }: { id: string }, +): Promise<{ link: string }> { + return makeRestApiRequest(context, 'GET', `/users/${id}/password-reset-link`); +} + export async function submitPersonalizationSurvey( context: IRestApiContext, params: IPersonalizationLatestVersion, diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index 1d98ae32c1ad7..9deed663d1d9c 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -7,7 +7,7 @@ >
, validator: (value: CodeExecutionMode): boolean => CODE_EXECUTION_MODES.includes(value), @@ -97,9 +100,6 @@ export default defineComponent({ }, computed: { ...mapStores(useRootStore), - isCloud() { - return useSettingsStore().deploymentType === 'cloud'; - }, content(): string { if (!this.editor) return ''; diff --git a/packages/editor-ui/src/components/ExecutionsUsage.vue b/packages/editor-ui/src/components/ExecutionsUsage.vue index 523928672248d..207342e6c0fb1 100644 --- a/packages/editor-ui/src/components/ExecutionsUsage.vue +++ b/packages/editor-ui/src/components/ExecutionsUsage.vue @@ -66,8 +66,8 @@ import { i18n as locale } from '@/plugins/i18n'; import { DateTime } from 'luxon'; import type { CloudPlanAndUsageData } from '@/Interface'; -import { CLOUD_CHANGE_PLAN_PAGE } from '@/constants'; import { computed } from 'vue'; +import { useUIStore } from '@/stores'; const PROGRESS_BAR_MINIMUM_THRESHOLD = 8; @@ -114,7 +114,7 @@ const maxExecutions = computed(() => { }); const onUpgradeClicked = () => { - location.href = CLOUD_CHANGE_PLAN_PAGE; + useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect'); }; diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 303489203761d..887c4d977e46a 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -162,6 +162,7 @@ import { getWorkflowPermissions } from '@/permissions'; import { useUsersStore } from '@/stores/users.store'; import { useUsageStore } from '@/stores/usage.store'; import { createEventBus } from 'n8n-design-system'; +import { useCloudPlanStore } from '@/stores'; const hasChanged = (prev: string[], curr: string[]) => { if (prev.length !== curr.length) { @@ -212,6 +213,7 @@ export default defineComponent({ useUsageStore, useWorkflowsStore, useUsersStore, + useCloudPlanStore, ), currentUser(): IUser | null { return this.usersStore.currentUser; diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 244570a4d3f28..682c465dbae56 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -91,6 +91,7 @@ :defaultValue="parameter.default" :language="editorLanguage" :isReadOnly="isReadOnly" + :aiButtonEnabled="settingsStore.isCloudDeployment" @valueChanged="valueChangedDebounced" /> @@ -387,6 +388,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useCredentialsStore } from '@/stores/credentials.store'; +import { useSettingsStore } from '@/stores/settings.store'; import { htmlEditorEventBus } from '@/event-bus'; import Vue from 'vue'; @@ -519,7 +521,13 @@ export default defineComponent({ }, }, computed: { - ...mapStores(useCredentialsStore, useNodeTypesStore, useNDVStore, useWorkflowsStore), + ...mapStores( + useCredentialsStore, + useNodeTypesStore, + useNDVStore, + useWorkflowsStore, + useSettingsStore, + ), expressionDisplayValue(): string { if (this.forceShowExpression) { return ''; diff --git a/packages/editor-ui/src/mixins/nodeHelpers.ts b/packages/editor-ui/src/mixins/nodeHelpers.ts index 8f7dec8ffda7d..4609b4f653297 100644 --- a/packages/editor-ui/src/mixins/nodeHelpers.ts +++ b/packages/editor-ui/src/mixins/nodeHelpers.ts @@ -36,6 +36,7 @@ import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings.store'; import { useUsersStore } from '@/stores/users.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useRootStore } from '@/stores'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useCredentialsStore } from '@/stores/credentials.store'; import { defineComponent } from 'vue'; @@ -49,6 +50,7 @@ export const nodeHelpers = defineComponent({ useSettingsStore, useWorkflowsStore, useUsersStore, + useRootStore, ), }, methods: { @@ -524,6 +526,9 @@ export const nodeHelpers = defineComponent({ data as INode, nodeType.subtitle, 'internal', + this.rootStore.timezone, + {}, + undefined, PLACEHOLDER_FILLED_AT_EXECUTION_TIME, ) as string | undefined; } diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 1fa2b2bec4beb..2e7ef0feef885 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1169,6 +1169,9 @@ "settings.users.actions.delete": "Delete User", "settings.users.actions.reinvite": "Resend Invite", "settings.users.actions.copyInviteLink": "Copy Invite Link", + "settings.users.actions.copyPasswordResetLink": "Copy Password Reset Link", + "settings.users.actions.allowSSOManualLogin": "Allow Manual Login", + "settings.users.actions.disallowSSOManualLogin": "Disallow Manual Login", "settings.users.deleteWorkflowsAndCredentials": "Delete their workflows and credentials", "settings.users.emailInvitesSent": "An invite email was sent to {emails}", "settings.users.emailInvitesSentError": "Could not invite {emails}", @@ -1187,6 +1190,12 @@ "settings.users.inviteXUser.inviteUrl": "Create {count} invite links", "settings.users.inviteUrlCreated": "Invite link copied to clipboard", "settings.users.inviteUrlCreated.message": "Send the invite link to your invitee for activation", + "settings.users.passwordResetUrlCreated": "Password reset link copied to clipboard", + "settings.users.passwordResetUrlCreated.message": "Send the reset link to your user for them to reset their password", + "settings.users.allowSSOManualLogin": "Manual Login Allowed", + "settings.users.allowSSOManualLogin.message": "User can now login manually and through SSO", + "settings.users.disallowSSOManualLogin": "Manual Login Disallowed", + "settings.users.disallowSSOManualLogin.message": "User must now login through SSO only", "settings.users.multipleInviteUrlsCreated": "Invite links created", "settings.users.multipleInviteUrlsCreated.message": "Send the invite links to your invitees for activation", "settings.users.newEmailsToInvite": "New User Email Addresses", diff --git a/packages/editor-ui/src/stores/cloudPlan.store.ts b/packages/editor-ui/src/stores/cloudPlan.store.ts index b034d711b0380..6bf6465d9cd76 100644 --- a/packages/editor-ui/src/stores/cloudPlan.store.ts +++ b/packages/editor-ui/src/stores/cloudPlan.store.ts @@ -45,7 +45,7 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => { return state.usage?.executions >= state.data?.monthlyExecutionsLimit; }); - const getOwnerCurrentPLan = async () => { + const getOwnerCurrentPlan = async () => { const cloudUserId = settingsStore.settings.n8nMetadata?.userId; const hasCloudPlan = usersStore.currentUser?.isOwner && settingsStore.isCloudDeployment && cloudUserId; @@ -70,10 +70,31 @@ export const useCloudPlanStore = defineStore('cloudPlan', () => { return usage; }; + const usageLeft = computed(() => { + if (!state.data || !state.usage) return { workflowsLeft: -1, executionsLeft: -1 }; + + return { + workflowsLeft: state.data.activeWorkflowsLimit - state.usage.activeWorkflows, + executionsLeft: state.data.monthlyExecutionsLimit - state.usage.executions, + }; + }); + + const trialDaysLeft = computed(() => { + if (!state.data?.expirationDate) return -1; + + const differenceInMs = new Date().valueOf() - new Date(state.data.expirationDate).valueOf(); + + const differenceInDays = Math.floor(differenceInMs / (1000 * 60 * 60 * 24)); + + return Math.ceil(differenceInDays); + }); + return { state, - getOwnerCurrentPLan, + getOwnerCurrentPlan, getInstanceCurrentUsage, + usageLeft, + trialDaysLeft, userIsTrialing, currentPlanData, currentUsageData, diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 4d3a7f966c88e..5eee35de2a827 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -48,10 +48,11 @@ import { useRootStore } from './n8nRoot.store'; import { getCurlToJson } from '@/api/curlHelper'; import { useWorkflowsStore } from './workflows.store'; import { useSettingsStore } from './settings.store'; -import { useTelemetryStore } from './telemetry.store'; +import { useUsageStore } from './usage.store'; import { useCloudPlanStore } from './cloudPlan.store'; import type { BaseTextKey } from '@/plugins/i18n'; import { i18n as locale } from '@/plugins/i18n'; +import { useTelemetryStore } from '@/stores/telemetry.store'; export const useUIStore = defineStore(STORES.UI, { state: (): UIState => ({ @@ -479,15 +480,22 @@ export const useUIStore = defineStore(STORES.UI, { const rootStore = useRootStore(); return getCurlToJson(rootStore.getRestApiContext, curlCommand); }, - goToUpgrade(source: string, utm_campaign: string): void { - const cloudPlanStore = useCloudPlanStore(); + goToUpgrade(source: string, utm_campaign: string, mode: 'open' | 'redirect' = 'open'): void { + const { usageLeft, trialDaysLeft, userIsTrialing } = useCloudPlanStore(); + const { executionsLeft, workflowsLeft } = usageLeft; useTelemetryStore().track('User clicked upgrade CTA', { source, + isTrial: userIsTrialing, deploymentType: useSettingsStore().deploymentType, - isTrial: cloudPlanStore.userIsTrialing, - trialDaysLeft: + trialDaysLeft, + executionsLeft, + workflowsLeft, }); - window.open(this.upgradeLinkUrl(source, utm_campaign), '_blank'); + if (mode === 'open') { + window.open(this.upgradeLinkUrl(source, utm_campaign), '_blank'); + } else { + location.href = this.upgradeLinkUrl(source, utm_campaign); + } }, }, }); diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts index 6f8d203764c38..200deaa2a06f6 100644 --- a/packages/editor-ui/src/stores/users.store.ts +++ b/packages/editor-ui/src/stores/users.store.ts @@ -2,6 +2,7 @@ import { changePassword, deleteUser, getInviteLink, + getPasswordResetLink, getUsers, inviteUsers, login, @@ -17,6 +18,7 @@ import { updateCurrentUser, updateCurrentUserPassword, updateCurrentUserSettings, + updateOtherUserSettings, validatePasswordToken, validateSignupToken, } from '@/api/users'; @@ -251,6 +253,19 @@ export const useUsersStore = defineStore(STORES.USERS, { this.addUsers([this.currentUser]); } }, + async updateOtherUserSettings( + userId: string, + settings: IUserResponse['settings'], + ): Promise { + const rootStore = useRootStore(); + const updatedSettings = await updateOtherUserSettings( + rootStore.getRestApiContext, + userId, + settings, + ); + this.users[userId].settings = updatedSettings; + this.addUsers([this.users[userId]]); + }, async updateCurrentUserPassword({ password, currentPassword, @@ -288,6 +303,10 @@ export const useUsersStore = defineStore(STORES.USERS, { const rootStore = useRootStore(); return getInviteLink(rootStore.getRestApiContext, params); }, + async getUserPasswordResetLink(params: { id: string }): Promise<{ link: string }> { + const rootStore = useRootStore(); + return getPasswordResetLink(rootStore.getRestApiContext, params); + }, async submitPersonalizationSurvey(results: IPersonalizationLatestVersion): Promise { const rootStore = useRootStore(); await submitPersonalizationSurvey(rootStore.getRestApiContext, results); diff --git a/packages/editor-ui/src/views/SettingsApiView.vue b/packages/editor-ui/src/views/SettingsApiView.vue index 0e8b2acd7e939..ff78c47e3c782 100644 --- a/packages/editor-ui/src/views/SettingsApiView.vue +++ b/packages/editor-ui/src/views/SettingsApiView.vue @@ -90,10 +90,10 @@ import CopyInput from '@/components/CopyInput.vue'; import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings.store'; import { useRootStore } from '@/stores/n8nRoot.store'; +import { useUIStore } from '@/stores/ui.store'; import { useUsersStore } from '@/stores/users.store'; +import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { DOCS_DOMAIN, MODAL_CONFIRM } from '@/constants'; -import { useCloudPlanStore } from '@/stores'; -import { CLOUD_CHANGE_PLAN_PAGE } from '@/constants'; export default defineComponent({ name: 'SettingsApiView', @@ -104,6 +104,7 @@ export default defineComponent({ return { ...useToast(), ...useMessage(), + ...useUIStore(), }; }, data() { @@ -126,7 +127,7 @@ export default defineComponent({ : `https://${DOCS_DOMAIN}/api/api-reference/`; }, computed: { - ...mapStores(useRootStore, useSettingsStore, useUsersStore, useCloudPlanStore), + ...mapStores(useRootStore, useSettingsStore, useUsersStore, useCloudPlanStore, useUIStore), currentUser(): IUser | null { return this.usersStore.currentUser; }, @@ -139,7 +140,7 @@ export default defineComponent({ }, methods: { onUpgrade() { - location.href = CLOUD_CHANGE_PLAN_PAGE; + this.uiStore.goToUpgrade('settings-n8n-api', 'upgrade-api', 'redirect'); }, async showDeleteModal() { const confirmed = await this.confirm( diff --git a/packages/editor-ui/src/views/SettingsUsersView.vue b/packages/editor-ui/src/views/SettingsUsersView.vue index 0bc2c6b1a0dc6..9c9779a6ae9ad 100644 --- a/packages/editor-ui/src/views/SettingsUsersView.vue +++ b/packages/editor-ui/src/views/SettingsUsersView.vue @@ -50,9 +50,13 @@ :actions="usersListActions" :users="usersStore.allUsers" :currentUserId="usersStore.currentUserId" + :isSamlLoginEnabled="ssoStore.isSamlLoginEnabled" @delete="onDelete" @reinvite="onReinvite" @copyInviteLink="onCopyInviteLink" + @copyPasswordResetLink="onCopyPasswordResetLink" + @allowSSOManualLogin="onAllowSSOManualLogin" + @disallowSSOManualLogin="onDisallowSSOManualLogin" />
@@ -106,6 +110,22 @@ export default defineComponent({ label: this.$locale.baseText('settings.users.actions.delete'), value: 'delete', }, + { + label: this.$locale.baseText('settings.users.actions.copyPasswordResetLink'), + value: 'copyPasswordResetLink', + }, + { + label: this.$locale.baseText('settings.users.actions.allowSSOManualLogin'), + value: 'allowSSOManualLogin', + guard: (user) => + this.settingsStore.isSamlLoginEnabled && !user.settings?.allowSSOManualLogin, + }, + { + label: this.$locale.baseText('settings.users.actions.disallowSSOManualLogin'), + value: 'disallowSSOManualLogin', + guard: (user) => + this.settingsStore.isSamlLoginEnabled && user.settings?.allowSSOManualLogin === true, + }, ]; }, }, @@ -152,8 +172,46 @@ export default defineComponent({ }); } }, + async onCopyPasswordResetLink(userId: string) { + const user = this.usersStore.getUserById(userId) as IUser | null; + if (user) { + const url = await this.usersStore.getUserPasswordResetLink(user); + this.copyToClipboard(url.link); + + this.showToast({ + type: 'success', + title: this.$locale.baseText('settings.users.passwordResetUrlCreated'), + message: this.$locale.baseText('settings.users.passwordResetUrlCreated.message'), + }); + } + }, + async onAllowSSOManualLogin(userId: string) { + const user = this.usersStore.getUserById(userId) as IUser | null; + if (user?.settings) { + user.settings.allowSSOManualLogin = true; + await this.usersStore.updateOtherUserSettings(userId, user.settings); + + this.showToast({ + type: 'success', + title: this.$locale.baseText('settings.users.allowSSOManualLogin'), + message: this.$locale.baseText('settings.users.allowSSOManualLogin.message'), + }); + } + }, + async onDisallowSSOManualLogin(userId: string) { + const user = this.usersStore.getUserById(userId) as IUser | null; + if (user?.settings) { + user.settings.allowSSOManualLogin = false; + await this.usersStore.updateOtherUserSettings(userId, user.settings); + this.showToast({ + type: 'success', + title: this.$locale.baseText('settings.users.disallowSSOManualLogin'), + message: this.$locale.baseText('settings.users.disallowSSOManualLogin.message'), + }); + } + }, goToUpgrade() { - this.uiStore.goToUpgrade('users', 'upgrade-users'); + this.uiStore.goToUpgrade('settings-users', 'upgrade-users'); }, }, }); diff --git a/packages/nodes-base/nodes/Code/Code.node.ts b/packages/nodes-base/nodes/Code/Code.node.ts index 0cd52d3af8806..3af95ccccb4ca 100644 --- a/packages/nodes-base/nodes/Code/Code.node.ts +++ b/packages/nodes-base/nodes/Code/Code.node.ts @@ -102,13 +102,18 @@ export class Code implements INodeType { const getSandbox = (index = 0) => { const code = this.getNodeParameter(codeParameterName, index) as string; const context = getSandboxContext.call(this, index); + if (nodeMode === 'runOnceForAllItems') { + context.items = context.$input.all(); + } else { + context.item = context.$input.item; + } + if (language === 'python') { const modules = this.getNodeParameter('modules', index) as string; const moduleImports: string[] = modules ? modules.split(',').map((m) => m.trim()) : []; context.printOverwrite = workflowMode === 'manual' ? this.sendMessageToUI : null; return new PythonSandbox(context, code, moduleImports, index, this.helpers); } else { - context.items = context.$input.all(); const sandbox = new JavaScriptSandbox(context, code, index, workflowMode, this.helpers); if (workflowMode === 'manual') { sandbox.vm.on('console.log', this.sendMessageToUI); diff --git a/packages/nodes-base/nodes/Code/test/Code.workflow.json b/packages/nodes-base/nodes/Code/test/Code.workflow.json index 02bef9a1648b2..07933b0f824a9 100644 --- a/packages/nodes-base/nodes/Code/test/Code.workflow.json +++ b/packages/nodes-base/nodes/Code/test/Code.workflow.json @@ -1,56 +1,64 @@ { - "name": "My workflow 24", "nodes": [ { "parameters": {}, - "id": "3691826c-caf3-4773-b0af-f5fe2eda42bc", + "id": "33eede8d-2ab0-42ab-b79a-a069d8549ab0", "name": "When clicking \"Execute Workflow\"", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, - "position": [ - -960, - 600 - ] + "position": [-40, 580] }, { "parameters": { "jsCode": "return[\n { value: 1 },\n { value: 2 },\n]" }, - "id": "149e8223-20e2-480e-b675-2aeb6a9f9095", + "id": "a5913b52-24dc-4f81-bb7f-f90e61dad978", "name": "Sample Data", "type": "n8n-nodes-base.code", "typeVersion": 1, - "position": [ - -720, - 600 - ] + "position": [200, 580] }, { "parameters": { "jsCode": "// Loop over input items and add a new field\n// called 'myNewField' to the JSON of each one\nlet sum = 0;\nfor (const item of $input.all()) {\n sum += item.json.value;\n}\n\nreturn [ {sum} ];" }, - "id": "2002ec71-fd88-4a5b-b2fe-0071fc5397e4", + "id": "c4ad4913-5af3-42bc-a784-69182f1facdd", "name": "Run Once for All Items", "type": "n8n-nodes-base.code", "typeVersion": 1, - "position": [ - -460, - 480 - ] + "position": [460, 320] + }, + { + "parameters": { + "jsCode": "// Loop over input items and add a new field\n// called 'myNewField' to the JSON of each one\nlet sum = 0;\nfor (const item of items) {\n sum += item.json.value;\n}\n\nreturn [ {sum} ];" + }, + "id": "34cbd204-4335-4790-92cd-c3df617eee21", + "name": "Run Once for All Items (Legacy Syntax)", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [460, 500] }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "// Add a new field called 'myNewField' to the\n// JSON of the item\n$input.item.json.myNewField = $input.item.json.value;\n\nreturn $input.item;" }, - "id": "9adbeb7a-c711-4ff6-881e-96d5e122c2bc", + "id": "f67d29bf-554a-4572-8867-4456182dec24", "name": "Run Once for Each Item", "type": "n8n-nodes-base.code", "typeVersion": 1, - "position": [ - -460, - 720 - ] + "position": [460, 680] + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "// Add a new field called 'myNewField' to the\n// JSON of the item\nitem.json.myNewField = item.json.value;\n\nreturn item;" + }, + "id": "6f4bf149-e84e-4e0d-802a-7eaf7a42b18c", + "name": "Run Once for Each Item (Legacy Syntax)", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [460, 860] } ], "pinData": { @@ -74,6 +82,27 @@ "myNewField": 2 } } + ], + "Run Once for All Items (Legacy Syntax)": [ + { + "json": { + "sum": 3 + } + } + ], + "Run Once for Each Item (Legacy Syntax)": [ + { + "json": { + "value": 1, + "myNewField": 1 + } + }, + { + "json": { + "value": 2, + "myNewField": 2 + } + } ] }, "connections": { @@ -100,17 +129,19 @@ "node": "Run Once for Each Item", "type": "main", "index": 0 + }, + { + "node": "Run Once for All Items (Legacy Syntax)", + "type": "main", + "index": 0 + }, + { + "node": "Run Once for Each Item (Legacy Syntax)", + "type": "main", + "index": 0 } ] ] } - }, - "active": false, - "settings": {}, - "versionId": "b0d8ec77-92ab-4fa7-93b1-8a2e3543059d", - "id": "181", - "meta": { - "instanceId": "104a4d08d8897b8bdeb38aaca515021075e0bd8544c983c2bb8c86e6a8e6081c" - }, - "tags": [] -} \ No newline at end of file + } +} diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 60a3555546a82..f71244e31611e 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1961,6 +1961,14 @@ export interface IUserManagementSettings { authenticationMethod: AuthenticationMethod; } +export interface IUserSettings { + isOnboarded?: boolean; + showUserActivationSurvey?: boolean; + firstSuccessfulWorkflowId?: string; + userActivated?: boolean; + allowSSOManualLogin?: boolean; +} + export interface IPublicApiSettings { enabled: boolean; latestVersion: number; diff --git a/patches/typedi@0.10.0.patch b/patches/typedi@0.10.0.patch index 38f3b73e28c34..fcbd9d56d722d 100644 --- a/patches/typedi@0.10.0.patch +++ b/patches/typedi@0.10.0.patch @@ -1,8 +1,16 @@ diff --git a/cjs/container-instance.class.js b/cjs/container-instance.class.js -index e473b1e652aa0b6e7462f7ba93fcef2812483b20..1e2ac7e5cb7943f5226a2bc25fa83bee0470f90c 100644 +index e473b1e652aa0b6e7462f7ba93fcef2812483b20..1e406113d68c401ee170c997afb53e5f71edeee2 100644 --- a/cjs/container-instance.class.js +++ b/cjs/container-instance.class.js -@@ -234,6 +234,7 @@ class ContainerInstance { +@@ -209,6 +209,7 @@ class ContainerInstance { + // this allows us to support javascript where we don't have decorators and emitted metadata about dependencies + // need to be injected, and user can use provided container to get instances he needs + params.push(this); ++ if (process.env.NODE_ENV === 'production') Object.freeze(constructableTargetType.prototype); + value = new constructableTargetType(...params); + // TODO: Calling this here, leads to infinite loop, because @Inject decorator registerds a handler + // TODO: which calls Container.get, which will check if the requested type has a value set and if not +@@ -234,6 +235,7 @@ class ContainerInstance { */ initializeParams(target, paramTypes) { return paramTypes.map((paramType, index) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bdc3c8cf9e81..d6314632f0e2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ patchedDependencies: hash: prckukfdop5sl2her6de25cod4 path: patches/element-ui@2.15.12.patch typedi@0.10.0: - hash: syy565ld7euwcedfbmx53j2qc4 + hash: 62r6bc2crgimafeyruodhqlgo4 path: patches/typedi@0.10.0.patch importers: @@ -456,7 +456,7 @@ importers: version: 1.1.1 typedi: specifier: ^0.10.0 - version: 0.10.0(patch_hash=syy565ld7euwcedfbmx53j2qc4) + version: 0.10.0(patch_hash=62r6bc2crgimafeyruodhqlgo4) typeorm: specifier: ^0.3.12 version: 0.3.12(ioredis@5.2.4)(mysql2@2.3.3)(pg@8.8.0)(sqlite3@5.1.6) @@ -21852,7 +21852,7 @@ packages: /typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - /typedi@0.10.0(patch_hash=syy565ld7euwcedfbmx53j2qc4): + /typedi@0.10.0(patch_hash=62r6bc2crgimafeyruodhqlgo4): resolution: {integrity: sha512-v3UJF8xm68BBj6AF4oQML3ikrfK2c9EmZUyLOfShpJuItAqVBHWP/KtpGinkSsIiP6EZyyb6Z3NXyW9dgS9X1w==} dev: false patched: true