From afda049491de1e1c5853767a4251a7a3e1d2e295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 27 Sep 2024 13:32:12 +0200 Subject: [PATCH 01/87] refactor(core): Separate execution `startedAt` from `createdAt` (#10810) --- packages/cli/src/active-executions.ts | 11 +++-- .../concurrency-control.service.ts | 1 - .../databases/entities/execution-entity.ts | 9 +++- ...0136-SeparateExecutionCreationFromStart.ts | 27 ++++++++++++ .../src/databases/migrations/mysqldb/index.ts | 2 + .../databases/migrations/postgresdb/index.ts | 2 + .../src/databases/migrations/sqlite/index.ts | 2 + .../repositories/execution.repository.ts | 42 +++++++++++++------ .../shared/shared-hook-functions.ts | 4 +- .../cli/src/executions/execution.service.ts | 5 +-- packages/cli/src/interfaces.ts | 10 +++-- packages/cli/src/scaling/job-processor.ts | 4 +- .../src/workflow-execute-additional-data.ts | 8 +++- .../workflows/workflow-execution.service.ts | 5 +-- .../execution.service.integration.test.ts | 2 + .../credentials-risk-reporter.test.ts | 2 + .../test/integration/shared/db/executions.ts | 1 + .../design-system/src/css/_tokens.dark.scss | 1 + packages/design-system/src/css/_tokens.scss | 1 + .../global/GlobalExecutionsListItem.vue | 4 +- .../workflow/WorkflowExecutionsCard.test.ts | 21 ++++++++++ .../workflow/WorkflowExecutionsCard.vue | 26 ++++++++++-- .../src/composables/useExecutionHelpers.ts | 2 + .../src/plugins/i18n/locales/en.json | 3 ++ .../src/utils/formatters/dateFormatter.ts | 4 ++ packages/workflow/src/Interfaces.ts | 2 + 26 files changed, 163 insertions(+), 38 deletions(-) create mode 100644 packages/cli/src/databases/migrations/common/1727427440136-SeparateExecutionCreationFromStart.ts diff --git a/packages/cli/src/active-executions.ts b/packages/cli/src/active-executions.ts index 8f7661925b..933bf9a944 100644 --- a/packages/cli/src/active-executions.ts +++ b/packages/cli/src/active-executions.ts @@ -13,7 +13,7 @@ import { Service } from 'typedi'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { ExecutionNotFoundError } from '@/errors/execution-not-found-error'; import type { - ExecutionPayload, + CreateExecutionPayload, IExecutingWorkflowData, IExecutionDb, IExecutionsCurrentSummary, @@ -52,11 +52,10 @@ export class ActiveExecutions { if (executionId === undefined) { // Is a new execution so save in DB - const fullExecutionData: ExecutionPayload = { + const fullExecutionData: CreateExecutionPayload = { data: executionData.executionData!, mode, finished: false, - startedAt: new Date(), workflowData: executionData.workflowData, status: executionStatus, workflowId: executionData.workflowData.id, @@ -74,7 +73,10 @@ export class ActiveExecutions { executionId = await this.executionRepository.createNewExecution(fullExecutionData); assert(executionId); - await this.concurrencyControl.throttle({ mode, executionId }); + if (config.getEnv('executions.mode') === 'regular') { + await this.concurrencyControl.throttle({ mode, executionId }); + await this.executionRepository.setRunning(executionId); + } executionStatus = 'running'; } else { // Is an existing execution we want to finish so update in DB @@ -86,6 +88,7 @@ export class ActiveExecutions { data: executionData.executionData!, waitTill: null, status: executionStatus, + // this is resuming, so keep `startedAt` as it was }; await this.executionRepository.updateExistingExecution(executionId, execution); diff --git a/packages/cli/src/concurrency/concurrency-control.service.ts b/packages/cli/src/concurrency/concurrency-control.service.ts index 45ef2e1206..db7826a258 100644 --- a/packages/cli/src/concurrency/concurrency-control.service.ts +++ b/packages/cli/src/concurrency/concurrency-control.service.ts @@ -70,7 +70,6 @@ export class ConcurrencyControlService { this.productionQueue.on('execution-released', async (executionId) => { this.log('Execution released', { executionId }); - await this.executionRepository.resetStartedAt(executionId); }); } diff --git a/packages/cli/src/databases/entities/execution-entity.ts b/packages/cli/src/databases/entities/execution-entity.ts index f481bb97f4..7b63b63eaf 100644 --- a/packages/cli/src/databases/entities/execution-entity.ts +++ b/packages/cli/src/databases/entities/execution-entity.ts @@ -47,7 +47,14 @@ export class ExecutionEntity { status: ExecutionStatus; @Column(datetimeColumnType) - startedAt: Date; + createdAt: Date; + + /** + * Time when the processing of the execution actually started. This column + * is `null` when an execution is enqueued but has not started yet. + */ + @Column({ type: datetimeColumnType, nullable: true }) + startedAt: Date | null; @Index() @Column({ type: datetimeColumnType, nullable: true }) diff --git a/packages/cli/src/databases/migrations/common/1727427440136-SeparateExecutionCreationFromStart.ts b/packages/cli/src/databases/migrations/common/1727427440136-SeparateExecutionCreationFromStart.ts new file mode 100644 index 0000000000..a44450fa2f --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1727427440136-SeparateExecutionCreationFromStart.ts @@ -0,0 +1,27 @@ +import type { MigrationContext, ReversibleMigration } from '@/databases/types'; + +export class SeparateExecutionCreationFromStart1727427440136 implements ReversibleMigration { + async up({ + schemaBuilder: { addColumns, column, dropNotNull }, + runQuery, + escape, + }: MigrationContext) { + await addColumns('execution_entity', [ + column('createdAt').notNull.timestamp().default('NOW()'), + ]); + + await dropNotNull('execution_entity', 'startedAt'); + + const executionEntity = escape.tableName('execution_entity'); + const createdAt = escape.columnName('createdAt'); + const startedAt = escape.columnName('startedAt'); + + // inaccurate for pre-migration rows but prevents `createdAt` from being nullable + await runQuery(`UPDATE ${executionEntity} SET ${createdAt} = ${startedAt};`); + } + + async down({ schemaBuilder: { dropColumns, addNotNull } }: MigrationContext) { + await dropColumns('execution_entity', ['createdAt']); + await addNotNull('execution_entity', 'startedAt'); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index 288f18edbe..07b910b949 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -64,6 +64,7 @@ import { CreateInvalidAuthTokenTable1723627610222 } from '../common/172362761022 import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices'; import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables'; import { AddApiKeysTable1724951148974 } from '../common/1724951148974-AddApiKeysTable'; +import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -130,4 +131,5 @@ export const mysqlMigrations: Migration[] = [ RefactorExecutionIndices1723796243146, CreateAnnotationTables1724753530828, AddApiKeysTable1724951148974, + SeparateExecutionCreationFromStart1727427440136, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 077d686b7e..21b90e201d 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -64,6 +64,7 @@ import { CreateInvalidAuthTokenTable1723627610222 } from '../common/172362761022 import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices'; import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables'; import { AddApiKeysTable1724951148974 } from '../common/1724951148974-AddApiKeysTable'; +import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -130,4 +131,5 @@ export const postgresMigrations: Migration[] = [ RefactorExecutionIndices1723796243146, CreateAnnotationTables1724753530828, AddApiKeysTable1724951148974, + SeparateExecutionCreationFromStart1727427440136, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index 62fda4b7d0..2828bb3f59 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -61,6 +61,7 @@ import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101 import { CreateInvalidAuthTokenTable1723627610222 } from '../common/1723627610222-CreateInvalidAuthTokenTable'; import { RefactorExecutionIndices1723796243146 } from '../common/1723796243146-RefactorExecutionIndices'; import { CreateAnnotationTables1724753530828 } from '../common/1724753530828-CreateExecutionAnnotationTables'; +import { SeparateExecutionCreationFromStart1727427440136 } from '../common/1727427440136-SeparateExecutionCreationFromStart'; const sqliteMigrations: Migration[] = [ InitialMigration1588102412422, @@ -124,6 +125,7 @@ const sqliteMigrations: Migration[] = [ RefactorExecutionIndices1723796243146, CreateAnnotationTables1724753530828, AddApiKeysTable1724951148974, + SeparateExecutionCreationFromStart1727427440136, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index d76d78c99a..7a67b71ace 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -42,7 +42,7 @@ import { ExecutionAnnotation } from '@/databases/entities/execution-annotation.e import { PostgresLiveRowsRetrievalError } from '@/errors/postgres-live-rows-retrieval.error'; import type { ExecutionSummaries } from '@/executions/execution.types'; import type { - ExecutionPayload, + CreateExecutionPayload, IExecutionBase, IExecutionFlattedDb, IExecutionResponse, @@ -198,7 +198,7 @@ export class ExecutionRepository extends Repository { return executions.map((execution) => { const { executionData, ...rest } = execution; return rest; - }); + }) as IExecutionFlattedDb[] | IExecutionResponse[] | IExecutionBase[]; } reportInvalidExecutions(executions: ExecutionEntity[]) { @@ -297,15 +297,15 @@ export class ExecutionRepository extends Repository { }), ...(options?.includeAnnotation && serializedAnnotation && { annotation: serializedAnnotation }), - }; + } as IExecutionFlattedDb | IExecutionResponse | IExecutionBase; } /** * Insert a new execution and its execution data using a transaction. */ - async createNewExecution(execution: ExecutionPayload): Promise { + async createNewExecution(execution: CreateExecutionPayload): Promise { const { data, workflowData, ...rest } = execution; - const { identifiers: inserted } = await this.insert(rest); + const { identifiers: inserted } = await this.insert({ ...rest, createdAt: new Date() }); const { id: executionId } = inserted[0] as { id: string }; const { connections, nodes, name, settings } = workflowData ?? {}; await this.executionDataRepository.insert({ @@ -344,16 +344,25 @@ export class ExecutionRepository extends Repository { await this.update({ id: executionId }, { status }); } - async resetStartedAt(executionId: string) { - await this.update({ id: executionId }, { startedAt: new Date() }); + async setRunning(executionId: string) { + const startedAt = new Date(); + + await this.update({ id: executionId }, { status: 'running', startedAt }); + + return startedAt; } async updateExistingExecution(executionId: string, execution: Partial) { - // Se isolate startedAt because it must be set when the execution starts and should never change. - // So we prevent updating it, if it's sent (it usually is and causes problems to executions that - // are resumed after waiting for some time, as a new startedAt is set) - const { id, data, workflowId, workflowData, startedAt, customData, ...executionInformation } = - execution; + const { + id, + data, + workflowId, + workflowData, + createdAt, // must never change + startedAt, // must never change + customData, + ...executionInformation + } = execution; if (Object.keys(executionInformation).length > 0) { await this.update({ id: executionId }, executionInformation); } @@ -721,6 +730,7 @@ export class ExecutionRepository extends Repository { mode: true, retryOf: true, status: true, + createdAt: true, startedAt: true, stoppedAt: true, }; @@ -806,6 +816,7 @@ export class ExecutionRepository extends Repository { // @tech_debt: These transformations should not be needed private toSummary(execution: { id: number | string; + createdAt?: Date | string; startedAt?: Date | string; stoppedAt?: Date | string; waitTill?: Date | string | null; @@ -817,6 +828,13 @@ export class ExecutionRepository extends Repository { return date; }; + if (execution.createdAt) { + execution.createdAt = + execution.createdAt instanceof Date + ? execution.createdAt.toISOString() + : normalizeDateString(execution.createdAt); + } + if (execution.startedAt) { execution.startedAt = execution.startedAt instanceof Date diff --git a/packages/cli/src/execution-lifecycle-hooks/shared/shared-hook-functions.ts b/packages/cli/src/execution-lifecycle-hooks/shared/shared-hook-functions.ts index d6d55e63e5..2888f7bd28 100644 --- a/packages/cli/src/execution-lifecycle-hooks/shared/shared-hook-functions.ts +++ b/packages/cli/src/execution-lifecycle-hooks/shared/shared-hook-functions.ts @@ -3,7 +3,7 @@ import type { ExecutionStatus, IRun, IWorkflowBase } from 'n8n-workflow'; import { Container } from 'typedi'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; -import type { ExecutionPayload, IExecutionDb } from '@/interfaces'; +import type { IExecutionDb, UpdateExecutionPayload } from '@/interfaces'; import { Logger } from '@/logger'; import { ExecutionMetadataService } from '@/services/execution-metadata.service'; import { isWorkflowIdValid } from '@/utils'; @@ -46,7 +46,7 @@ export function prepareExecutionDataForDbUpdate(parameters: { 'pinData', ]); - const fullExecutionData: ExecutionPayload = { + const fullExecutionData: UpdateExecutionPayload = { data: runData.data, mode: runData.mode, finished: runData.finished ? runData.finished : false, diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index 53023fce9a..be685befc9 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -32,7 +32,7 @@ import { QueuedExecutionRetryError } from '@/errors/queued-execution-retry.error import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { - ExecutionPayload, + CreateExecutionPayload, IExecutionFlattedResponse, IExecutionResponse, IWorkflowDb, @@ -321,11 +321,10 @@ export class ExecutionService { }, }; - const fullExecutionData: ExecutionPayload = { + const fullExecutionData: CreateExecutionPayload = { data: executionData, mode, finished: false, - startedAt: new Date(), workflowData, workflowId: workflow.id, stoppedAt: new Date(), diff --git a/packages/cli/src/interfaces.ts b/packages/cli/src/interfaces.ts index 4d767862bb..5c29eea093 100644 --- a/packages/cli/src/interfaces.ts +++ b/packages/cli/src/interfaces.ts @@ -115,6 +115,7 @@ export type SaveExecutionDataType = 'all' | 'none'; export interface IExecutionBase { id: string; mode: WorkflowExecuteMode; + createdAt: Date; // set by DB startedAt: Date; stoppedAt?: Date; // empty value means execution is still running workflowId: string; @@ -131,10 +132,11 @@ export interface IExecutionDb extends IExecutionBase { workflowData: IWorkflowBase; } -/** - * Payload for creating or updating an execution. - */ -export type ExecutionPayload = Omit; +/** Payload for creating an execution. */ +export type CreateExecutionPayload = Omit; + +/** Payload for updating an execution. */ +export type UpdateExecutionPayload = Omit; export interface IExecutionResponse extends IExecutionBase { id: string; diff --git a/packages/cli/src/scaling/job-processor.ts b/packages/cli/src/scaling/job-processor.ts index 3155b0d90f..1d0bccc6bf 100644 --- a/packages/cli/src/scaling/job-processor.ts +++ b/packages/cli/src/scaling/job-processor.ts @@ -47,7 +47,7 @@ export class JobProcessor { this.logger.info(`[JobProcessor] Starting job ${job.id} (execution ${executionId})`); - await this.executionRepository.updateStatus(executionId, 'running'); + const startedAt = await this.executionRepository.setRunning(executionId); let { staticData } = execution.workflowData; @@ -137,7 +137,7 @@ export class JobProcessor { workflowId: execution.workflowId, workflowName: execution.workflowData.name, mode: execution.mode, - startedAt: execution.startedAt, + startedAt, retryOf: execution.retryOf ?? '', status: execution.status, }; diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 4d3bd7a223..bc39620c20 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -41,7 +41,11 @@ import config from '@/config'; import { CredentialsHelper } from '@/credentials-helper'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { ExternalHooks } from '@/external-hooks'; -import type { IWorkflowExecuteProcess, IWorkflowErrorData, ExecutionPayload } from '@/interfaces'; +import type { + IWorkflowExecuteProcess, + IWorkflowErrorData, + UpdateExecutionPayload, +} from '@/interfaces'; import { NodeTypes } from '@/node-types'; import { Push } from '@/push'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; @@ -865,7 +869,7 @@ async function executeWorkflow( // Therefore, database might not contain finished errors. // Force an update to db as there should be no harm doing this - const fullExecutionData: ExecutionPayload = { + const fullExecutionData: UpdateExecutionPayload = { data: fullRunData.data, mode: fullRunData.mode, finished: fullRunData.finished ? fullRunData.finished : false, diff --git a/packages/cli/src/workflows/workflow-execution.service.ts b/packages/cli/src/workflows/workflow-execution.service.ts index 4dc6d00f34..8b1206b22d 100644 --- a/packages/cli/src/workflows/workflow-execution.service.ts +++ b/packages/cli/src/workflows/workflow-execution.service.ts @@ -22,7 +22,7 @@ import type { Project } from '@/databases/entities/project'; import type { User } from '@/databases/entities/user'; import { ExecutionRepository } from '@/databases/repositories/execution.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; -import type { ExecutionPayload, IWorkflowDb, IWorkflowErrorData } from '@/interfaces'; +import type { CreateExecutionPayload, IWorkflowDb, IWorkflowErrorData } from '@/interfaces'; import { Logger } from '@/logger'; import { NodeTypes } from '@/node-types'; import { SubworkflowPolicyChecker } from '@/subworkflows/subworkflow-policy-checker.service'; @@ -206,11 +206,10 @@ export class WorkflowExecutionService { initialNode, ); - const fullExecutionData: ExecutionPayload = { + const fullExecutionData: CreateExecutionPayload = { data: fakeExecution.data, mode: fakeExecution.mode, finished: false, - startedAt: new Date(), stoppedAt: new Date(), workflowData, waitTill: null, diff --git a/packages/cli/test/integration/execution.service.integration.test.ts b/packages/cli/test/integration/execution.service.integration.test.ts index 15d97f69ab..22d0d65754 100644 --- a/packages/cli/test/integration/execution.service.integration.test.ts +++ b/packages/cli/test/integration/execution.service.integration.test.ts @@ -70,6 +70,7 @@ describe('ExecutionService', () => { mode: expect.any(String), retryOf: null, status: expect.any(String), + createdAt: expect.any(String), startedAt: expect.any(String), stoppedAt: expect.any(String), waitTill: null, @@ -510,6 +511,7 @@ describe('ExecutionService', () => { mode: expect.any(String), retryOf: null, status: expect.any(String), + createdAt: expect.any(String), startedAt: expect.any(String), stoppedAt: expect.any(String), waitTill: null, diff --git a/packages/cli/test/integration/security-audit/credentials-risk-reporter.test.ts b/packages/cli/test/integration/security-audit/credentials-risk-reporter.test.ts index 8da1f3e1bf..4513beb6bb 100644 --- a/packages/cli/test/integration/security-audit/credentials-risk-reporter.test.ts +++ b/packages/cli/test/integration/security-audit/credentials-risk-reporter.test.ts @@ -159,6 +159,7 @@ test('should report credential in not recently executed workflow', async () => { const savedExecution = await Container.get(ExecutionRepository).save({ finished: true, mode: 'manual', + createdAt: date, startedAt: date, stoppedAt: date, workflowId: workflow.id, @@ -227,6 +228,7 @@ test('should not report credentials in recently executed workflow', async () => const savedExecution = await Container.get(ExecutionRepository).save({ finished: true, mode: 'manual', + createdAt: date, startedAt: date, stoppedAt: date, workflowId: workflow.id, diff --git a/packages/cli/test/integration/shared/db/executions.ts b/packages/cli/test/integration/shared/db/executions.ts index e09ca44dfb..4dd0b4fa76 100644 --- a/packages/cli/test/integration/shared/db/executions.ts +++ b/packages/cli/test/integration/shared/db/executions.ts @@ -39,6 +39,7 @@ export async function createExecution( const execution = await Container.get(ExecutionRepository).save({ finished: finished ?? true, mode: mode ?? 'manual', + createdAt: new Date(), startedAt: startedAt ?? new Date(), ...(workflow !== undefined && { workflowId: workflow.id }), stoppedAt: stoppedAt ?? new Date(), diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index 8377f524c6..72963efcf5 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -212,6 +212,7 @@ --execution-selector-background: var(--prim-gray-740); --execution-selector-text: var(--color-text-base); --execution-select-all-text: var(--color-text-base); + --execution-card-text-waiting: var(--prim-color-secondary-tint-100); // NDV --color-run-data-background: var(--prim-gray-800); diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index 1690926232..56d5142c87 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -273,6 +273,7 @@ --execution-card-border-running: var(--prim-color-alt-b-tint-250); --execution-card-border-unknown: var(--prim-gray-120); --execution-card-background-hover: var(--color-foreground-light); + --execution-card-text-waiting: var(--color-secondary); --execution-selector-background: var(--color-background-dark); --execution-selector-text: var(--color-text-xlight); --execution-select-all-text: var(--color-danger); diff --git a/packages/editor-ui/src/components/executions/global/GlobalExecutionsListItem.vue b/packages/editor-ui/src/components/executions/global/GlobalExecutionsListItem.vue index 43d85e8e27..f23dbbc4ac 100644 --- a/packages/editor-ui/src/components/executions/global/GlobalExecutionsListItem.vue +++ b/packages/editor-ui/src/components/executions/global/GlobalExecutionsListItem.vue @@ -62,7 +62,9 @@ const classes = computed(() => { }); const formattedStartedAtDate = computed(() => { - return props.execution.startedAt ? formatDate(props.execution.startedAt) : ''; + return props.execution.startedAt + ? formatDate(props.execution.startedAt) + : i18n.baseText('executionsList.startingSoon'); }); const formattedWaitTillDate = computed(() => { diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.test.ts b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.test.ts index e1a34eaef6..b9b9645b0a 100644 --- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.test.ts +++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.test.ts @@ -150,4 +150,25 @@ describe('WorkflowExecutionsCard', () => { } }, ); + + test('displays correct text for new execution', () => { + const createdAt = new Date('2024-09-27T12:00:00Z'); + const props = { + execution: { + id: '1', + mode: 'manual', + status: 'new', + createdAt: createdAt.toISOString(), + }, + workflowPermissions: { + execute: true, + }, + }; + + const { getByTestId } = renderComponent({ props }); + + const executionTimeElement = getByTestId('execution-time'); + expect(executionTimeElement).toBeVisible(); + expect(executionTimeElement.textContent).toBe('27 Sep - Starting soon'); + }); }); diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue index 5c74cc2594..18ba430cb9 100644 --- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue +++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue @@ -11,6 +11,7 @@ import { useI18n } from '@/composables/useI18n'; import type { PermissionsRecord } from '@/permissions'; import { usePostHog } from '@/stores/posthog.store'; import { useSettingsStore } from '@/stores/settings.store'; +import { toDayMonth, toTime } from '@/utils/formatters/dateFormatter'; const props = defineProps<{ execution: ExecutionSummary; @@ -87,7 +88,17 @@ function onRetryMenuItemSelect(action: string): void { :data-test-execution-status="executionUIDetails.name" >
- + + {{ toDayMonth(executionUIDetails.createdAt) }} - + {{ locale.baseText('executionDetails.startingSoon') }} + + {{ executionUIDetails.startTime }}
@@ -106,6 +117,15 @@ function onRetryMenuItemSelect(action: string): void { {{ locale.baseText('executionDetails.runningTimeRunning') }} + + {{ locale.baseText('executionDetails.at') }} {{ toTime(execution.createdAt) }} + dateformat(fullDate, 'd mmm'); + +export const toTime = (fullDate: Date | string) => dateformat(fullDate, 'HH:MM:ss'); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 62cf8c5bed..15802fe0b0 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2122,6 +2122,7 @@ export interface IWorkflowBase { name: string; active: boolean; createdAt: Date; + startedAt?: Date; updatedAt: Date; nodes: INode[]; connections: IConnections; @@ -2463,6 +2464,7 @@ export interface ExecutionSummary { retryOf?: string | null; retrySuccessId?: string | null; waitTill?: Date; + createdAt?: Date; startedAt: Date; stoppedAt?: Date; workflowId: string; From 2af0fbf52f0b404697f5148f81ad0035c9ffb6b9 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: Fri, 27 Sep 2024 13:36:43 +0200 Subject: [PATCH 02/87] fix(core): Upgrade @n8n/typeorm to address a rare mutex release issue (#10993) --- packages/@n8n/nodes-langchain/package.json | 2 +- packages/cli/package.json | 2 +- pnpm-lock.yaml | 152 +++++++-------------- 3 files changed, 48 insertions(+), 108 deletions(-) diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index ec7001286d..07362f85f9 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -150,7 +150,7 @@ "@langchain/redis": "0.1.0", "@langchain/textsplitters": "0.1.0", "@mozilla/readability": "^0.5.0", - "@n8n/typeorm": "0.3.20-10", + "@n8n/typeorm": "0.3.20-12", "@n8n/vm2": "3.9.25", "@pinecone-database/pinecone": "3.0.3", "@qdrant/js-client-rest": "1.11.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6d11b9cf55..7b030b3eb9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -92,7 +92,7 @@ "@n8n/localtunnel": "3.0.0", "@n8n/n8n-nodes-langchain": "workspace:*", "@n8n/permissions": "workspace:*", - "@n8n/typeorm": "0.3.20-10", + "@n8n/typeorm": "0.3.20-12", "@n8n_io/ai-assistant-sdk": "1.9.4", "@n8n_io/license-sdk": "2.13.1", "@oclif/core": "4.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5cacb2c5d9..21df434299 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -462,8 +462,8 @@ importers: specifier: ^0.5.0 version: 0.5.0 '@n8n/typeorm': - specifier: 0.3.20-10 - version: 0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.6.2)) + specifier: 0.3.20-12 + version: 0.3.20-12(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.6.2)) '@n8n/vm2': specifier: 3.9.25 version: 3.9.25 @@ -699,8 +699,8 @@ importers: specifier: workspace:* version: link:../@n8n/permissions '@n8n/typeorm': - specifier: 0.3.20-10 - version: 0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.6.2)) + specifier: 0.3.20-12 + version: 0.3.20-12(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.6.2)) '@n8n_io/ai-assistant-sdk': specifier: 1.9.4 version: 1.9.4 @@ -3831,15 +3831,15 @@ packages: resolution: {integrity: sha512-IPBHa7gC0wwHVct/dnBquHz+uMCDZaZ05cor1D/rjlwaOe/PVu5mtoZaPHYuR98R3W1/IyxC5PuBd0JizDP9gg==} engines: {node: '>=20.15', pnpm: '>=9.5'} - '@n8n/typeorm@0.3.20-10': - resolution: {integrity: sha512-YYQKkafEGqNAG+VgtGbJOWpcyF4ZsRJ+Q7qXigTXZFQb4xpL/+t0BXEMhy8Gw0OEjnZI5cbLGHcQtj7Xlfg7dw==} + '@n8n/typeorm@0.3.20-12': + resolution: {integrity: sha512-Jc+Uys9HXTRq+u2XTqnAqjZVvAPwYH4qy4wRcizN0u7sfBvRGRpeF8ZAoplOGjXPRBG278QKcfVAJ64j/bj+uQ==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: '@google-cloud/spanner': ^5.18.0 '@libsql/client': ^0.4.2 '@sap/hana-client': ^2.12.25 - '@sentry/node': ^7.87.0 + '@sentry/node': <=8.x better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0 hdb-pool: ^0.1.6 ioredis: ^5.0.4 @@ -9605,10 +9605,6 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} - minipass@7.0.2: - resolution: {integrity: sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==} - engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -10319,10 +10315,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} - path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -12820,7 +12812,7 @@ snapshots: '@acuminous/bitsyntax@0.1.2': dependencies: buffer-more-ints: 1.0.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 safe-buffer: 5.1.2 transitivePeerDependencies: - supports-color @@ -12962,7 +12954,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.654.0(@aws-sdk/client-sts@3.645.0) '@aws-sdk/client-sts': 3.654.0 '@aws-sdk/core': 3.654.0 - '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.645.0) '@aws-sdk/middleware-host-header': 3.654.0 '@aws-sdk/middleware-logger': 3.654.0 '@aws-sdk/middleware-recursion-detection': 3.654.0 @@ -13107,7 +13099,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.654.0(@aws-sdk/client-sts@3.654.0) '@aws-sdk/client-sts': 3.654.0 '@aws-sdk/core': 3.654.0 - '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.645.0) '@aws-sdk/middleware-host-header': 3.654.0 '@aws-sdk/middleware-logger': 3.654.0 '@aws-sdk/middleware-recursion-detection': 3.654.0 @@ -13617,7 +13609,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-sdk/client-sso-oidc': 3.654.0(@aws-sdk/client-sts@3.654.0) '@aws-sdk/core': 3.654.0 - '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.654.0) + '@aws-sdk/credential-provider-node': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.645.0) '@aws-sdk/middleware-host-header': 3.654.0 '@aws-sdk/middleware-logger': 3.654.0 '@aws-sdk/middleware-recursion-detection': 3.654.0 @@ -13834,24 +13826,6 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-ini@3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.654.0)': - dependencies: - '@aws-sdk/client-sts': 3.654.0 - '@aws-sdk/credential-provider-env': 3.654.0 - '@aws-sdk/credential-provider-http': 3.654.0 - '@aws-sdk/credential-provider-process': 3.654.0 - '@aws-sdk/credential-provider-sso': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0)) - '@aws-sdk/credential-provider-web-identity': 3.654.0(@aws-sdk/client-sts@3.654.0) - '@aws-sdk/types': 3.654.0 - '@smithy/credential-provider-imds': 3.2.3 - '@smithy/property-provider': 3.1.6 - '@smithy/shared-ini-file-loader': 3.1.7 - '@smithy/types': 3.4.2 - tslib: 2.6.2 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - '@aws-sdk/credential-provider-node@3.478.0': dependencies: '@aws-sdk/credential-provider-env': 3.468.0 @@ -13945,25 +13919,6 @@ snapshots: - '@aws-sdk/client-sts' - aws-crt - '@aws-sdk/credential-provider-node@3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.654.0)': - dependencies: - '@aws-sdk/credential-provider-env': 3.654.0 - '@aws-sdk/credential-provider-http': 3.654.0 - '@aws-sdk/credential-provider-ini': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0))(@aws-sdk/client-sts@3.654.0) - '@aws-sdk/credential-provider-process': 3.654.0 - '@aws-sdk/credential-provider-sso': 3.654.0(@aws-sdk/client-sso-oidc@3.654.0(@aws-sdk/client-sts@3.654.0)) - '@aws-sdk/credential-provider-web-identity': 3.654.0(@aws-sdk/client-sts@3.654.0) - '@aws-sdk/types': 3.654.0 - '@smithy/credential-provider-imds': 3.2.3 - '@smithy/property-provider': 3.1.6 - '@smithy/shared-ini-file-loader': 3.1.7 - '@smithy/types': 3.4.2 - tslib: 2.6.2 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - '@aws-sdk/client-sts' - - aws-crt - '@aws-sdk/credential-provider-process@3.468.0': dependencies: '@aws-sdk/types': 3.468.0 @@ -14062,14 +14017,6 @@ snapshots: '@smithy/types': 3.4.2 tslib: 2.6.2 - '@aws-sdk/credential-provider-web-identity@3.654.0(@aws-sdk/client-sts@3.654.0)': - dependencies: - '@aws-sdk/client-sts': 3.654.0 - '@aws-sdk/types': 3.654.0 - '@smithy/property-provider': 3.1.6 - '@smithy/types': 3.4.2 - tslib: 2.6.2 - '@aws-sdk/credential-providers@3.645.0': dependencies: '@aws-sdk/client-cognito-identity': 3.645.0 @@ -15191,7 +15138,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 @@ -15347,7 +15294,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -15377,7 +15324,7 @@ snapshots: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.10 '@iconify/types': 2.0.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 kolorist: 1.8.0 local-pkg: 0.5.0 mlly: 1.7.1 @@ -15671,7 +15618,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -16060,7 +16007,7 @@ snapshots: esprima-next: 5.8.4 recast: 0.22.0 - '@n8n/typeorm@0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.6.2))': + '@n8n/typeorm@0.3.20-12(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.6.2))': dependencies: '@n8n/p-retry': 6.2.0-2 '@sqltools/formatter': 1.2.5 @@ -16069,9 +16016,9 @@ snapshots: buffer: 6.0.3 chalk: 4.1.2 dayjs: 1.11.10 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 dotenv: 16.3.1 - glob: 10.3.10 + glob: 10.4.5 mkdirp: 2.1.3 reflect-metadata: 0.2.2 sha.js: 2.4.11 @@ -16091,7 +16038,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@n8n/typeorm@0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.6.2))': + '@n8n/typeorm@0.3.20-12(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.6.2))': dependencies: '@n8n/p-retry': 6.2.0-2 '@sqltools/formatter': 1.2.5 @@ -16100,9 +16047,9 @@ snapshots: buffer: 6.0.3 chalk: 4.1.2 dayjs: 1.11.10 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 dotenv: 16.3.1 - glob: 10.3.10 + glob: 10.4.5 mkdirp: 2.1.3 reflect-metadata: 0.2.2 sha.js: 2.4.11 @@ -18483,7 +18430,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.6.2) '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.6.2) - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 eslint: 8.57.0 ts-api-utils: 1.0.1(typescript@5.6.2) optionalDependencies: @@ -18499,7 +18446,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -18514,7 +18461,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.2.0 '@typescript-eslint/visitor-keys': 7.2.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -18904,19 +18851,19 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color agent-base@7.1.0: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color agentkeepalive@4.2.1: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 depd: 1.1.2 humanize-ms: 1.2.1 transitivePeerDependencies: @@ -20648,7 +20595,7 @@ snapshots: esbuild-register@3.5.0(esbuild@0.20.2): dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 esbuild: 0.20.2 transitivePeerDependencies: - supports-color @@ -21515,16 +21462,16 @@ snapshots: foreground-child: 3.1.1 jackspeak: 2.3.6 minimatch: 9.0.5 - minipass: 7.0.2 - path-scurry: 1.10.1 + minipass: 7.1.2 + path-scurry: 1.11.1 glob@10.3.3: dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 minimatch: 9.0.5 - minipass: 7.0.2 - path-scurry: 1.10.1 + minipass: 7.1.2 + path-scurry: 1.11.1 glob@10.4.5: dependencies: @@ -21834,7 +21781,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color optional: true @@ -21843,14 +21790,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color http-proxy-agent@7.0.0: dependencies: agent-base: 7.1.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -21865,14 +21812,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -22222,7 +22169,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -23430,8 +23377,6 @@ snapshots: minipass@5.0.0: {} - minipass@7.0.2: {} - minipass@7.1.2: {} minizlib@2.1.2: @@ -23482,7 +23427,7 @@ snapshots: dependencies: '@babel/runtime': 7.24.7 chokidar: 3.5.2 - glob: 10.3.10 + glob: 10.4.5 html-minifier: 4.0.0 js-beautify: 1.14.9 lodash: 4.17.21 @@ -23808,7 +23753,7 @@ snapshots: mqtt-packet@9.0.0: dependencies: bl: 6.0.12 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 process-nextick-args: 2.0.1 transitivePeerDependencies: - supports-color @@ -24093,7 +24038,7 @@ snapshots: number-allocator@1.0.14: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 js-sdsl: 4.3.0 transitivePeerDependencies: - supports-color @@ -24415,11 +24360,6 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.10.1: - dependencies: - lru-cache: 10.2.2 - minipass: 7.0.2 - path-scurry@1.11.1: dependencies: lru-cache: 10.2.2 @@ -25571,7 +25511,7 @@ snapshots: simple-websocket@9.1.0: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 queue-microtask: 1.2.3 randombytes: 2.1.0 readable-stream: 3.6.0 @@ -25651,7 +25591,7 @@ snapshots: socks-proxy-agent@6.2.1: dependencies: agent-base: 6.0.2 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -25957,7 +25897,7 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.5 commander: 4.1.1 - glob: 10.3.10 + glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.6 @@ -25967,7 +25907,7 @@ snapshots: dependencies: component-emitter: 1.3.0 cookiejar: 2.1.4 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 3.5.1 @@ -26686,7 +26626,7 @@ snapshots: vite-node@2.1.1(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1): dependencies: cac: 6.7.14 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 pathe: 1.1.2 vite: 5.4.6(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1) transitivePeerDependencies: From 2df5a5b649f8ba3b747782d6d5045820aa74955d Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:23:31 +0300 Subject: [PATCH 03/87] fix(Respond to Webhook Node): Node does not work with Wait node (#10992) --- .../RespondToWebhook/RespondToWebhook.node.ts | 18 +++++-- .../test/RespondToWebhook.test.ts | 47 +++++++++++++++++++ packages/workflow/src/Constants.ts | 3 ++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 packages/nodes-base/nodes/RespondToWebhook/test/RespondToWebhook.test.ts diff --git a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts index 68bd19e3f1..0ad0dd85c3 100644 --- a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts +++ b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts @@ -8,7 +8,16 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { jsonParse, BINARY_ENCODING, NodeOperationError, NodeConnectionType } from 'n8n-workflow'; +import { + jsonParse, + BINARY_ENCODING, + NodeOperationError, + NodeConnectionType, + WEBHOOK_NODE_TYPE, + FORM_TRIGGER_NODE_TYPE, + CHAT_TRIGGER_NODE_TYPE, + WAIT_NODE_TYPE, +} from 'n8n-workflow'; import set from 'lodash/set'; import jwt from 'jsonwebtoken'; import { formatPrivateKey, generatePairedItemData } from '../../utils/utilities'; @@ -291,9 +300,10 @@ export class RespondToWebhook implements INodeType { const nodeVersion = this.getNode().typeVersion; const WEBHOOK_NODE_TYPES = [ - 'n8n-nodes-base.webhook', - 'n8n-nodes-base.formTrigger', - '@n8n/n8n-nodes-langchain.chatTrigger', + WEBHOOK_NODE_TYPE, + FORM_TRIGGER_NODE_TYPE, + CHAT_TRIGGER_NODE_TYPE, + WAIT_NODE_TYPE, ]; try { diff --git a/packages/nodes-base/nodes/RespondToWebhook/test/RespondToWebhook.test.ts b/packages/nodes-base/nodes/RespondToWebhook/test/RespondToWebhook.test.ts new file mode 100644 index 0000000000..c090511552 --- /dev/null +++ b/packages/nodes-base/nodes/RespondToWebhook/test/RespondToWebhook.test.ts @@ -0,0 +1,47 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import { + WAIT_NODE_TYPE, + type IExecuteFunctions, + type INode, + type NodeTypeAndVersion, +} from 'n8n-workflow'; + +import { RespondToWebhook } from '../RespondToWebhook.node'; + +describe('RespondToWebhook Node', () => { + let respondToWebhook: RespondToWebhook; + let mockExecuteFunctions: MockProxy; + + beforeEach(() => { + respondToWebhook = new RespondToWebhook(); + mockExecuteFunctions = mock(); + }); + + describe('execute method', () => { + it('should throw an error if no WEBHOOK_NODE_TYPES in parents', async () => { + mockExecuteFunctions.getInputData.mockReturnValue([]); + mockExecuteFunctions.getNode.mockReturnValue(mock({ typeVersion: 1.1 })); + mockExecuteFunctions.getParentNodes.mockReturnValue([ + mock({ type: 'n8n-nodes-base.someNode' }), + ]); + + await expect(respondToWebhook.execute.call(mockExecuteFunctions)).rejects.toThrow( + 'No Webhook node found in the workflow', + ); + }); + it('should not throw an error if WEBHOOK_NODE_TYPES is in parents', async () => { + mockExecuteFunctions.getInputData.mockReturnValue([]); + mockExecuteFunctions.getNode.mockReturnValue(mock({ typeVersion: 1.1 })); + mockExecuteFunctions.getParentNodes.mockReturnValue([ + mock({ type: WAIT_NODE_TYPE }), + ]); + mockExecuteFunctions.getNodeParameter.mockReturnValue('text'); + mockExecuteFunctions.getNodeParameter.mockReturnValue({}); + mockExecuteFunctions.getNodeParameter.mockReturnValue('noData'); + mockExecuteFunctions.sendResponse.mockReturnValue(); + + await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow(); + }); + }); +}); diff --git a/packages/workflow/src/Constants.ts b/packages/workflow/src/Constants.ts index 95123c9793..6ec9d47bb6 100644 --- a/packages/workflow/src/Constants.ts +++ b/packages/workflow/src/Constants.ts @@ -38,6 +38,9 @@ export const FUNCTION_NODE_TYPE = 'n8n-nodes-base.function'; export const FUNCTION_ITEM_NODE_TYPE = 'n8n-nodes-base.functionItem'; export const MERGE_NODE_TYPE = 'n8n-nodes-base.merge'; export const AI_TRANSFORM_NODE_TYPE = 'n8n-nodes-base.aiTransform'; +export const FORM_TRIGGER_NODE_TYPE = 'n8n-nodes-base.formTrigger'; +export const CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTrigger'; +export const WAIT_NODE_TYPE = 'n8n-nodes-base.wait'; export const STARTING_NODE_TYPES = [ MANUAL_TRIGGER_NODE_TYPE, From 136d49132567558b7d27069c857c0e0bfee70ce2 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 27 Sep 2024 15:12:31 +0200 Subject: [PATCH 04/87] fix(editor): Allow resources to move between personal and team projects (#10683) Co-authored-by: Danny Martini --- cypress/composables/projects.ts | 2 - cypress/e2e/39-projects.cy.ts | 293 ++++++++++++++---- .../src/credentials/credentials.service.ee.ts | 8 - .../cli/src/workflows/workflow.service.ee.ts | 8 - .../credentials/credentials.api.ee.test.ts | 279 +++++++---------- .../workflows/workflows.controller.ee.test.ts | 284 +++++++---------- .../src/directives/n8n-truncate.test.ts | 6 +- packages/design-system/src/utils/index.ts | 1 + .../design-system/src/utils/string.test.ts | 4 +- packages/design-system/src/utils/string.ts | 2 +- .../src/components/InputNodeSelect.vue | 7 +- packages/editor-ui/src/components/Modals.vue | 11 - .../editor-ui/src/components/NodeSettings.vue | 27 +- .../Projects/ProjectCardBadge.test.ts | 3 +- .../components/Projects/ProjectCardBadge.vue | 2 +- .../ProjectMoveResourceConfirmModal.test.ts | 68 ---- .../ProjectMoveResourceConfirmModal.vue | 131 -------- .../Projects/ProjectMoveResourceModal.test.ts | 55 +++- .../Projects/ProjectMoveResourceModal.vue | 165 ++++++++-- .../ProjectMoveSuccessToastMessage.test.ts | 87 ++++++ .../ProjectMoveSuccessToastMessage.vue | 55 +++- .../Projects/ProjectSharingInfo.vue | 4 + .../forms/ResourceFiltersDropdown.vue | 9 +- packages/editor-ui/src/constants.ts | 1 - .../src/plugins/i18n/locales/en.json | 21 +- .../editor-ui/src/stores/projects.store.ts | 20 ++ packages/editor-ui/src/stores/ui.store.ts | 2 - .../src/views/ProjectSettings.test.ts | 6 +- .../editor-ui/src/views/ProjectSettings.vue | 6 +- .../editor-ui/src/views/WorkflowsView.test.ts | 2 +- .../editor-ui/src/views/WorkflowsView.vue | 4 +- 31 files changed, 840 insertions(+), 733 deletions(-) delete mode 100644 packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.test.ts delete mode 100644 packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.vue create mode 100644 packages/editor-ui/src/components/Projects/ProjectMoveSuccessToastMessage.test.ts diff --git a/cypress/composables/projects.ts b/cypress/composables/projects.ts index 84379088d1..da9c6fcc65 100644 --- a/cypress/composables/projects.ts +++ b/cypress/composables/projects.ts @@ -32,8 +32,6 @@ export const addProjectMember = (email: string, role?: string) => { } }; export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal'); -export const getResourceMoveConfirmModal = () => - cy.getByTestId('project-move-resource-confirm-modal'); export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select'); export function createProject(name: string) { diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts index 59ed6bcb84..4e3bb583df 100644 --- a/cypress/e2e/39-projects.cy.ts +++ b/cypress/e2e/39-projects.cy.ts @@ -1,5 +1,11 @@ import * as projects from '../composables/projects'; -import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants'; +import { + INSTANCE_ADMIN, + INSTANCE_MEMBERS, + INSTANCE_OWNER, + MANUAL_TRIGGER_NODE_NAME, + NOTION_NODE_NAME, +} from '../constants'; import { WorkflowsPage, WorkflowPage, @@ -481,44 +487,15 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Next")') + .find('button:contains("Move workflow")') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() .find('li') - .should('have.length', 2) - .first() - .should('contain.text', 'Project 1') - .click(); - projects.getResourceMoveModal().find('button:contains("Next")').click(); - - projects - .getResourceMoveConfirmModal() - .should('be.visible') - .find('button:contains("Confirm")') - .should('be.disabled'); - - projects - .getResourceMoveConfirmModal() - .find('input[type="checkbox"]') - .first() - .parents('label') - .click(); - projects - .getResourceMoveConfirmModal() - .find('button:contains("Confirm")') - .should('be.disabled'); - projects - .getResourceMoveConfirmModal() - .find('input[type="checkbox"]') - .last() - .parents('label') - .click(); - projects - .getResourceMoveConfirmModal() - .find('button:contains("Confirm")') - .should('not.be.disabled') + .should('have.length', 5) + .filter(':contains("Project 1")') .click(); + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); workflowsPage.getters .workflowCards() @@ -526,9 +503,77 @@ describe('Projects', { disableAutoLogin: true }, () => { .filter(':contains("Owned by me")') .should('not.exist'); - // Move the credential from Project 1 to Project 2 + // Move the workflow from Project 1 to Project 2 projects.getMenuItems().first().click(); workflowsPage.getters.workflowCards().should('have.length', 2); + workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); + workflowsPage.getters.workflowMoveButton().click(); + + projects + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move workflow")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(':contains("Project 2")') + .click(); + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + + // Move the workflow from Project 2 to a member user + projects.getMenuItems().last().click(); + workflowsPage.getters.workflowCards().should('have.length', 2); + workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); + workflowsPage.getters.workflowMoveButton().click(); + + projects + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move workflow")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(`:contains("${INSTANCE_MEMBERS[0].email}")`) + .click(); + + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + workflowsPage.getters.workflowCards().should('have.length', 1); + + // Move the workflow from member user back to Home + projects.getHomeButton().click(); + workflowsPage.getters + .workflowCards() + .should('have.length', 3) + .filter(':has(.n8n-badge:contains("Project"))') + .should('have.length', 2); + workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); + workflowsPage.getters.workflowMoveButton().click(); + + projects + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move workflow")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(`:contains("${INSTANCE_OWNER.email}")`) + .click(); + + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + workflowsPage.getters + .workflowCards() + .should('have.length', 3) + .filter(':contains("Owned by me")') + .should('have.length', 1); + + // Move the credential from Project 1 to Project 2 + projects.getMenuItems().first().click(); projects.getProjectTabCredentials().click(); credentialsPage.getters.credentialCards().should('have.length', 1); credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); @@ -537,48 +582,162 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Next")') + .find('button:contains("Move credential")') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() .find('li') - .should('have.length', 1) - .first() - .should('contain.text', 'Project 2') + .should('have.length', 5) + .filter(':contains("Project 2")') .click(); - projects.getResourceMoveModal().find('button:contains("Next")').click(); + projects.getResourceMoveModal().find('button:contains("Move credential")').click(); - projects - .getResourceMoveConfirmModal() - .should('be.visible') - .find('button:contains("Confirm")') - .should('be.disabled'); - - projects - .getResourceMoveConfirmModal() - .find('input[type="checkbox"]') - .first() - .parents('label') - .click(); - projects - .getResourceMoveConfirmModal() - .find('button:contains("Confirm")') - .should('be.disabled'); - projects - .getResourceMoveConfirmModal() - .find('input[type="checkbox"]') - .last() - .parents('label') - .click(); - projects - .getResourceMoveConfirmModal() - .find('button:contains("Confirm")') - .should('not.be.disabled') - .click(); credentialsPage.getters.credentialCards().should('not.have.length'); + + // Move the credential from Project 2 to admin user projects.getMenuItems().last().click(); projects.getProjectTabCredentials().click(); credentialsPage.getters.credentialCards().should('have.length', 2); + + credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); + credentialsPage.getters.credentialMoveButton().click(); + + projects + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move credential")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(`:contains("${INSTANCE_ADMIN.email}")`) + .click(); + projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + credentialsPage.getters.credentialCards().should('have.length', 1); + + // Move the credential from admin user back to instance owner + projects.getHomeButton().click(); + projects.getProjectTabCredentials().click(); + credentialsPage.getters.credentialCards().should('have.length', 3); + + credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); + credentialsPage.getters.credentialMoveButton().click(); + + projects + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move credential")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(`:contains("${INSTANCE_OWNER.email}")`) + .click(); + projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + + credentialsPage.getters + .credentialCards() + .should('have.length', 3) + .filter(':contains("Owned by me")') + .should('have.length', 2); + + // Move the credential from admin user back to its original project (Project 1) + credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); + credentialsPage.getters.credentialMoveButton().click(); + + projects + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move credential")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(':contains("Project 1")') + .click(); + projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + + projects.getMenuItems().first().click(); + projects.getProjectTabCredentials().click(); + credentialsPage.getters + .credentialCards() + .filter(':contains("Credential in Project 1")') + .should('have.length', 1); + }); + + it('should allow to change inaccessible credential when the workflow was moved to a team project', () => { + cy.signinAsOwner(); + cy.visit(workflowsPage.url); + + // Create a credential in the Home project + projects.getProjectTabCredentials().should('be.visible').click(); + credentialsPage.getters.emptyListCreateCredentialButton().click(); + projects.createCredential('Credential in Home project'); + + // Create a workflow in the Home project + projects.getHomeButton().click(); + workflowsPage.getters.workflowCards().should('not.have.length'); + workflowsPage.getters.newWorkflowButtonCard().click(); + workflowsPage.getters.workflowCards().should('not.have.length'); + + workflowsPage.getters.newWorkflowButtonCard().click(); + workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); + ndv.getters.backToCanvas().click(); + workflowPage.actions.saveWorkflowOnButtonClick(); + + // Create a project and add a user to it + projects.createProject('Project 1'); + projects.addProjectMember(INSTANCE_MEMBERS[0].email); + projects.getProjectSettingsSaveButton().click(); + + // Move the workflow from Home to Project 1 + projects.getHomeButton().click(); + workflowsPage.getters + .workflowCards() + .should('have.length', 1) + .filter(':contains("Owned by me")') + .should('exist'); + workflowsPage.getters.workflowCardActions('My workflow').click(); + workflowsPage.getters.workflowMoveButton().click(); + + projects + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move workflow")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 4) + .filter(':contains("Project 1")') + .click(); + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + + workflowsPage.getters + .workflowCards() + .should('have.length', 1) + .filter(':contains("Owned by me")') + .should('not.exist'); + + //Log out with instance owner and log in with the member user + mainSidebar.actions.openUserMenu(); + cy.getByTestId('user-menu-item-logout').click(); + + cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email); + cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password); + cy.getByTestId('form-submit-button').click(); + + // Open the moved workflow + workflowsPage.getters.workflowCards().should('have.length', 1); + workflowsPage.getters.workflowCards().first().click(); + + // Check if the credential can be changed + workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick(); + ndv.getters.credentialInput().find('input').should('be.enabled'); }); it('should handle viewer role', () => { diff --git a/packages/cli/src/credentials/credentials.service.ee.ts b/packages/cli/src/credentials/credentials.service.ee.ts index 116137374a..aad78fe7b7 100644 --- a/packages/cli/src/credentials/credentials.service.ee.ts +++ b/packages/cli/src/credentials/credentials.service.ee.ts @@ -157,14 +157,6 @@ export class EnterpriseCredentialsService { "You can't transfer a credential into the project that's already owning it.", ); } - if (sourceProject.type !== 'team' && sourceProject.type !== 'personal') { - throw new TransferCredentialError( - 'You can only transfer credentials out of personal or team projects.', - ); - } - if (destinationProject.type !== 'team') { - throw new TransferCredentialError('You can only transfer credentials into team projects.'); - } await this.sharedCredentialsRepository.manager.transaction(async (trx) => { // 6. transfer the credential diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index 5456ac6268..4e329e7464 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -285,14 +285,6 @@ export class EnterpriseWorkflowService { "You can't transfer a workflow into the project that's already owning it.", ); } - if (sourceProject.type !== 'team' && sourceProject.type !== 'personal') { - throw new TransferWorkflowError( - 'You can only transfer workflows out of personal or team projects.', - ); - } - if (destinationProject.type !== 'team') { - throw new TransferWorkflowError('You can only transfer workflows into team projects.'); - } // 6. deactivate workflow if necessary const wasActive = workflow.active; diff --git a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts index b1c0bfab75..5428cafbd4 100644 --- a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts +++ b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts @@ -3,6 +3,7 @@ import { Container } from 'typedi'; import config from '@/config'; import type { Project } from '@/databases/entities/project'; +import type { ProjectRole } from '@/databases/entities/project-relation'; import type { User } from '@/databases/entities/user'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; @@ -1118,18 +1119,6 @@ describe('PUT /:credentialId/transfer', () => { .expect(400); }); - test('cannot transfer into a personal project', async () => { - const credential = await saveCredential(randomCredentialPayload(), { - user: member, - }); - - await testServer - .authAgentFor(member) - .put(`/credentials/${credential.id}/transfer`) - .send({ destinationProjectId: memberPersonalProject.id }) - .expect(400); - }); - test('cannot transfer somebody elses credential', async () => { const destinationProject = await createTeamProject('Destination Project', member); @@ -1158,187 +1147,139 @@ describe('PUT /:credentialId/transfer', () => { .expect(404); }); - test('project:editors cannot transfer credentials', async () => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Source Project'); - await linkUserToProject(member, sourceProject, 'project:editor'); - - const credential = await saveCredential(randomCredentialPayload(), { - project: sourceProject, - }); - - const destinationProject = await createTeamProject('Destination Project', member); - - // - // ACT & ASSERT - // - await testServer - .authAgentFor(member) - .put(`/credentials/${credential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(403); - }); - - test('transferring from a personal project to a team project severs all sharings', async () => { - // - // ARRANGE - // - const credential = await saveCredential(randomCredentialPayload(), { user: member }); - - // these sharings should be deleted by the transfer - await shareCredentialWithUsers(credential, [anotherMember, owner]); - - const destinationProject = await createTeamProject('Destination Project', member); - - // - // ACT - // - const response = await testServer - .authAgentFor(member) - .put(`/credentials/${credential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response.body).toEqual({}); - - const allSharings = await getCredentialSharings(credential); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - credentialsId: credential.id, - role: 'credential:owner', - }); - }); - - test('can transfer from team to another team project', async () => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Team Project 1', member); - const credential = await saveCredential(randomCredentialPayload(), { - project: sourceProject, - }); - - const destinationProject = await createTeamProject('Team Project 2', member); - - // - // ACT - // - const response = await testServer - .authAgentFor(member) - .put(`/credentials/${credential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response.body).toEqual({}); - - const allSharings = await getCredentialSharings(credential); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - credentialsId: credential.id, - role: 'credential:owner', - }); - }); - - test.each([ - ['owners', () => owner], - ['admins', () => admin], - ])( - '%s can always transfer from any personal or team project into any team project', - async (_name, actor) => { + test.each(['project:editor', 'project:viewer'])( + '%ss cannot transfer credentials', + async (projectRole) => { // // ARRANGE // - const sourceProject = await createTeamProject('Source Project', member); - const teamCredential = await saveCredential(randomCredentialPayload(), { + const sourceProject = await createTeamProject('Source Project'); + await linkUserToProject(member, sourceProject, projectRole); + + const credential = await saveCredential(randomCredentialPayload(), { project: sourceProject, }); - const personalCredential = await saveCredential(randomCredentialPayload(), { user: member }); - const destinationProject = await createTeamProject('Destination Project', member); // + // ACT & ASSERT + // + await testServer + .authAgentFor(member) + .put(`/credentials/${credential.id}/transfer`) + .send({ destinationProjectId: destinationProject.id }) + .expect(403); + }, + ); + + test.each< + [ + // user role + 'owners' | 'admins', + // source project type + 'team' | 'personal', + // destination project type + 'team' | 'personal', + // actor + () => User, + // source project + () => Promise | Project, + // destination project + () => Promise | Project, + ] + >([ + // owner + [ + 'owners', + 'team', + 'team', + () => owner, + async () => await createTeamProject('Source Project'), + async () => await createTeamProject('Destination Project'), + ], + [ + 'owners', + 'team', + 'personal', + () => owner, + async () => await createTeamProject('Source Project'), + () => memberPersonalProject, + ], + [ + 'owners', + 'personal', + 'team', + () => owner, + () => memberPersonalProject, + async () => await createTeamProject('Destination Project'), + ], + + // admin + [ + 'admins', + 'team', + 'team', + () => admin, + async () => await createTeamProject('Source Project'), + async () => await createTeamProject('Destination Project'), + ], + [ + 'admins', + 'team', + 'personal', + () => admin, + async () => await createTeamProject('Source Project'), + () => memberPersonalProject, + ], + [ + 'admins', + 'personal', + 'team', + () => admin, + () => memberPersonalProject, + async () => await createTeamProject('Destination Project'), + ], + ])( + '%s can always transfer from a %s project to a %s project', + async ( + _roleName, + _sourceProjectName, + _destinationProjectName, + getUser, + getSourceProject, + getDestinationProject, + ) => { + // ARRANGE + const user = getUser(); + const sourceProject = await getSourceProject(); + const destinationProject = await getDestinationProject(); + + const credential = await saveCredential(randomCredentialPayload(), { + project: sourceProject, + }); + // ACT - // - const response1 = await testServer - .authAgentFor(actor()) - .put(`/credentials/${teamCredential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - const response2 = await testServer - .authAgentFor(actor()) - .put(`/credentials/${personalCredential.id}/transfer`) + const response = await testServer + .authAgentFor(user) + .put(`/credentials/${credential.id}/transfer`) .send({ destinationProjectId: destinationProject.id }) .expect(200); - // // ASSERT - // - expect(response1.body).toEqual({}); - expect(response2.body).toEqual({}); + expect(response.body).toEqual({}); { - const allSharings = await getCredentialSharings(teamCredential); + const allSharings = await getCredentialSharings(credential); expect(allSharings).toHaveLength(1); expect(allSharings[0]).toMatchObject({ projectId: destinationProject.id, - credentialsId: teamCredential.id, - role: 'credential:owner', - }); - } - - { - const allSharings = await getCredentialSharings(personalCredential); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - credentialsId: personalCredential.id, + credentialsId: credential.id, role: 'credential:owner', }); } }, ); - - test.each([ - ['owners', () => owner], - ['admins', () => admin], - ])('%s cannot transfer into personal projects', async (_name, actor) => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Source Project', member); - const teamCredential = await saveCredential(randomCredentialPayload(), { - project: sourceProject, - }); - - const personalCredential = await saveCredential(randomCredentialPayload(), { user: member }); - - const destinationProject = anotherMemberPersonalProject; - - // - // ACT & ASSERT - // - await testServer - .authAgentFor(actor()) - .put(`/credentials/${teamCredential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(400); - await testServer - .authAgentFor(actor()) - .put(`/credentials/${personalCredential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(400); - }); }); function validateMainCredentialData(credential: ListQuery.Credentials.WithOwnedByAndSharedWith) { diff --git a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts index c8f2db889f..2002843bfe 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts @@ -5,6 +5,7 @@ import { v4 as uuid } from 'uuid'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; import config from '@/config'; import type { Project } from '@/databases/entities/project'; +import type { ProjectRole } from '@/databases/entities/project-relation'; import type { User } from '@/databases/entities/user'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository'; @@ -1385,18 +1386,6 @@ describe('PUT /:workflowId/transfer', () => { .expect(400); }); - test('cannot transfer into a personal project', async () => { - const sourceProject = await createTeamProject('Team Project', member); - - const workflow = await createWorkflow({}, sourceProject); - - await testServer - .authAgentFor(member) - .put(`/workflows/${workflow.id}/transfer`) - .send({ destinationProjectId: memberPersonalProject.id }) - .expect(400); - }); - test('cannot transfer somebody elses workflow', async () => { const destinationProject = await createTeamProject('Team Project', member); @@ -1421,180 +1410,133 @@ describe('PUT /:workflowId/transfer', () => { .expect(404); }); - test('project:editors cannot transfer workflows', async () => { - // - // ARRANGE - // - const sourceProject = await createTeamProject(); - await linkUserToProject(member, sourceProject, 'project:editor'); - - const workflow = await createWorkflow({}, sourceProject); - - const destinationProject = await createTeamProject(); - await linkUserToProject(member, destinationProject, 'project:admin'); - - // - // ACT & ASSERT - // - await testServer - .authAgentFor(member) - .put(`/workflows/${workflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(403); - }); - - test('transferring from a personal project to a team project severs all sharings', async () => { - // - // ARRANGE - // - const workflow = await createWorkflow({}, member); - - // these sharings should be deleted by the transfer - await shareWorkflowWithUsers(workflow, [anotherMember, owner]); - - const destinationProject = await createTeamProject('Team Project', member); - - // - // ACT - // - const response = await testServer - .authAgentFor(member) - .put(`/workflows/${workflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response.body).toEqual({}); - - const allSharings = await getWorkflowSharing(workflow); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - workflowId: workflow.id, - role: 'workflow:owner', - }); - }); - - test('can transfer from team to another team project', async () => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Team Project 1', member); - const workflow = await createWorkflow({}, sourceProject); - - const destinationProject = await createTeamProject('Team Project 2', member); - - // - // ACT - // - const response = await testServer - .authAgentFor(member) - .put(`/workflows/${workflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response.body).toEqual({}); - - const allSharings = await getWorkflowSharing(workflow); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - workflowId: workflow.id, - role: 'workflow:owner', - }); - }); - - test.each([ - ['owners', () => owner], - ['admins', () => admin], - ])( - 'global %s can always transfer from any personal or team project into any team project', - async (_name, actor) => { + test.each(['project:editor', 'project:viewer'])( + '%ss cannot transfer workflows', + async (projectRole) => { // // ARRANGE // - const sourceProject = await createTeamProject('Source Project', member); - const teamWorkflow = await createWorkflow({}, sourceProject); + const sourceProject = await createTeamProject(); + await linkUserToProject(member, sourceProject, projectRole); - const personalWorkflow = await createWorkflow({}, member); + const workflow = await createWorkflow({}, sourceProject); - const destinationProject = await createTeamProject('Destination Project', member); + const destinationProject = await createTeamProject(); + await linkUserToProject(member, destinationProject, 'project:admin'); // - // ACT + // ACT & ASSERT // - const response1 = await testServer - .authAgentFor(actor()) - .put(`/workflows/${teamWorkflow.id}/transfer`) + await testServer + .authAgentFor(member) + .put(`/workflows/${workflow.id}/transfer`) .send({ destinationProjectId: destinationProject.id }) - .expect(200); - const response2 = await testServer - .authAgentFor(actor()) - .put(`/workflows/${personalWorkflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response1.body).toEqual({}); - expect(response2.body).toEqual({}); - - { - const allSharings = await getWorkflowSharing(teamWorkflow); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - workflowId: teamWorkflow.id, - role: 'workflow:owner', - }); - } - - { - const allSharings = await getWorkflowSharing(personalWorkflow); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - workflowId: personalWorkflow.id, - role: 'workflow:owner', - }); - } + .expect(403); }, ); - test.each([ - ['owners', () => owner], - ['admins', () => admin], - ])('global %s cannot transfer into personal projects', async (_name, actor) => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Source Project', member); - const teamWorkflow = await createWorkflow({}, sourceProject); + test.each< + [ + // user role + 'owners' | 'admins', + // source project type + 'team' | 'personal', + // destination project type + 'team' | 'personal', + // actor + () => User, + // source project + () => Promise | Project, + // destination project + () => Promise | Project, + ] + >([ + // owner + [ + 'owners', + 'team', + 'team', + () => owner, + async () => await createTeamProject('Source Project'), + async () => await createTeamProject('Destination Project'), + ], + [ + 'owners', + 'team', + 'personal', + () => owner, + async () => await createTeamProject('Source Project'), + () => memberPersonalProject, + ], + [ + 'owners', + 'personal', + 'team', + () => owner, + () => memberPersonalProject, + async () => await createTeamProject('Destination Project'), + ], - const personalWorkflow = await createWorkflow({}, member); + // admin + [ + 'admins', + 'team', + 'team', + () => admin, + async () => await createTeamProject('Source Project'), + async () => await createTeamProject('Destination Project'), + ], + [ + 'admins', + 'team', + 'personal', + () => admin, + async () => await createTeamProject('Source Project'), + () => memberPersonalProject, + ], + [ + 'admins', + 'personal', + 'team', + () => admin, + () => memberPersonalProject, + async () => await createTeamProject('Destination Project'), + ], + ])( + 'global %s can transfer workflows from a %s project to a %s project', + async ( + _roleName, + _sourceProjectName, + _destinationProjectName, + getActor, + getSourceProject, + getDestinationProject, + ) => { + // ARRANGE + const actor = getActor(); + const sourceProject = await getSourceProject(); + const destinationProject = await getDestinationProject(); + const workflow = await createWorkflow({}, sourceProject); - const destinationProject = anotherMemberPersonalProject; + // ACT + const response = await testServer + .authAgentFor(actor) + .put(`/workflows/${workflow.id}/transfer`) + .send({ destinationProjectId: destinationProject.id }) + .expect(200); - // - // ACT & ASSERT - // - await testServer - .authAgentFor(actor()) - .put(`/workflows/${teamWorkflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(400); - await testServer - .authAgentFor(actor()) - .put(`/workflows/${personalWorkflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(400); - }); + // ASSERT + expect(response.body).toEqual({}); + + const allSharings = await getWorkflowSharing(workflow); + expect(allSharings).toHaveLength(1); + expect(allSharings[0]).toMatchObject({ + projectId: destinationProject.id, + workflowId: workflow.id, + role: 'workflow:owner', + }); + }, + ); test('removes and re-adds the workflow from the active workflow manager during the transfer', async () => { // diff --git a/packages/design-system/src/directives/n8n-truncate.test.ts b/packages/design-system/src/directives/n8n-truncate.test.ts index 89cd771283..309cab4563 100644 --- a/packages/design-system/src/directives/n8n-truncate.test.ts +++ b/packages/design-system/src/directives/n8n-truncate.test.ts @@ -24,7 +24,7 @@ describe('Directive n8n-truncate', () => { }, }, ); - expect(html()).toBe('
This is a very long text that...
'); + expect(html()).toBe('
This is a very long text that ...
'); }); it('should truncate text to 30 chars in case of wrong argument', async () => { @@ -48,7 +48,7 @@ describe('Directive n8n-truncate', () => { }, }, ); - expect(html()).toBe('
This is a very long text that...
'); + expect(html()).toBe('
This is a very long text that ...
'); }); it('should truncate text to given length', async () => { @@ -72,6 +72,6 @@ describe('Directive n8n-truncate', () => { }, }, ); - expect(html()).toBe('
This is a very long text...
'); + expect(html()).toBe('
This is a very long text ...
'); }); }); diff --git a/packages/design-system/src/utils/index.ts b/packages/design-system/src/utils/index.ts index be6ddc6375..d029baaa26 100644 --- a/packages/design-system/src/utils/index.ts +++ b/packages/design-system/src/utils/index.ts @@ -5,3 +5,4 @@ export * from './typeguards'; export * from './uid'; export * from './valueByPath'; export * from './testUtils'; +export * from './string'; diff --git a/packages/design-system/src/utils/string.test.ts b/packages/design-system/src/utils/string.test.ts index 6f65775f9a..0517d260d4 100644 --- a/packages/design-system/src/utils/string.test.ts +++ b/packages/design-system/src/utils/string.test.ts @@ -4,13 +4,13 @@ describe('Utils string', () => { describe('truncate', () => { it('should truncate text to 30 chars by default', () => { expect(truncate('This is a very long text that should be truncated')).toBe( - 'This is a very long text that...', + 'This is a very long text that ...', ); }); it('should truncate text to given length', () => { expect(truncate('This is a very long text that should be truncated', 25)).toBe( - 'This is a very long text...', + 'This is a very long text ...', ); }); }); diff --git a/packages/design-system/src/utils/string.ts b/packages/design-system/src/utils/string.ts index 9170b57c00..1c2b2aecfd 100644 --- a/packages/design-system/src/utils/string.ts +++ b/packages/design-system/src/utils/string.ts @@ -1,2 +1,2 @@ export const truncate = (text: string, length = 30): string => - text.length > length ? text.slice(0, length).trim() + '...' : text; + text.length > length ? text.slice(0, length) + '...' : text; diff --git a/packages/editor-ui/src/components/InputNodeSelect.vue b/packages/editor-ui/src/components/InputNodeSelect.vue index c7550a76c6..d0ec011e07 100644 --- a/packages/editor-ui/src/components/InputNodeSelect.vue +++ b/packages/editor-ui/src/components/InputNodeSelect.vue @@ -7,6 +7,7 @@ import { isPresent } from '@/utils/typesUtils'; import type { IConnectedNode, Workflow } from 'n8n-workflow'; import { computed } from 'vue'; import NodeIcon from './NodeIcon.vue'; +import { truncate } from 'n8n-design-system'; type Props = { nodes: IConnectedNode[]; @@ -100,11 +101,7 @@ function getMultipleNodesText(nodeName: string): string { } function title(nodeName: string, length = 30) { - const truncated = nodeName.substring(0, length); - if (truncated.length < nodeName.length) { - return `${truncated}...`; - } - return truncated; + return truncate(nodeName, length); } function subtitle(nodeName: string, depth: number) { diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index 396e70ce02..a243ce13a5 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -31,7 +31,6 @@ import { WORKFLOW_HISTORY_VERSION_RESTORE, SETUP_CREDENTIALS_MODAL_KEY, PROJECT_MOVE_RESOURCE_MODAL, - PROJECT_MOVE_RESOURCE_CONFIRM_MODAL, PROMPT_MFA_CODE_MODAL_KEY, } from '@/constants'; @@ -66,7 +65,6 @@ import DebugPaywallModal from '@/components/DebugPaywallModal.vue'; import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue'; import SetupWorkflowCredentialsModal from '@/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue'; import ProjectMoveResourceModal from '@/components/Projects/ProjectMoveResourceModal.vue'; -import ProjectMoveResourceConfirmModal from '@/components/Projects/ProjectMoveResourceConfirmModal.vue'; import NewAssistantSessionModal from '@/components/AskAssistant/NewAssistantSessionModal.vue'; import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue'; @@ -249,15 +247,6 @@ import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue'; /> - - -