From 5cd64171f1b3f9c98cc5ed9bca9310cd4be780f7 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Wed, 5 Nov 2025 04:05:39 +0100 Subject: [PATCH] add initial implementation --- .../@n8n/db/src/entities/execution-data.ts | 36 +++++++++-- .../@n8n/db/src/entities/workflow-history.ts | 4 +- ...274-AddWorkflowVersionIdToExecutionData.ts | 11 ++++ .../@n8n/db/src/migrations/mysqldb/index.ts | 2 + .../db/src/migrations/postgresdb/index.ts | 2 + .../@n8n/db/src/migrations/sqlite/index.ts | 2 + .../repositories/execution-data.repository.ts | 18 +++++- .../src/repositories/execution.repository.ts | 9 ++- .../chat-hub/chat-hub-message.entity.ts | 63 +++++++++++++------ 9 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 packages/@n8n/db/src/migrations/common/1762310956274-AddWorkflowVersionIdToExecutionData.ts diff --git a/packages/@n8n/db/src/entities/execution-data.ts b/packages/@n8n/db/src/entities/execution-data.ts index 3134b2b107f..9ac8251be98 100644 --- a/packages/@n8n/db/src/entities/execution-data.ts +++ b/packages/@n8n/db/src/entities/execution-data.ts @@ -1,9 +1,19 @@ -import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from '@n8n/typeorm'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToOne, + OneToOne, + PrimaryColumn, + Relation, +} from '@n8n/typeorm'; import { IWorkflowBase } from 'n8n-workflow'; import { JsonColumn } from './abstract-entity'; import { ExecutionEntity } from './execution-entity'; import { ISimplifiedPinData } from './types-db'; +import { WorkflowHistory } from './workflow-history'; import { idStringifier } from '../utils/transformers'; @Entity() @@ -24,14 +34,30 @@ export class ExecutionData { @JsonColumn() workflowData: Omit & { pinData?: ISimplifiedPinData }; + @Column({ type: 'varchar', length: 36, nullable: true }) + workflowVersionId: string | null; + + @ManyToOne(() => WorkflowHistory, { onDelete: 'SET NULL', nullable: true }) + @JoinTable({ + joinColumn: { + name: 'workflowVersionId', + referencedColumnName: 'versionId', + }, + }) + workflowHistory?: Relation | null; + @PrimaryColumn({ transformer: idStringifier }) executionId: string; - @OneToOne('ExecutionEntity', 'executionData', { - onDelete: 'CASCADE', - }) + @OneToOne( + () => ExecutionEntity, + (ee) => ee.executionData, + { + onDelete: 'CASCADE', + }, + ) @JoinColumn({ name: 'executionId', }) - execution: ExecutionEntity; + execution: Relation; } diff --git a/packages/@n8n/db/src/entities/workflow-history.ts b/packages/@n8n/db/src/entities/workflow-history.ts index cc4aa169bcf..a06adadf849 100644 --- a/packages/@n8n/db/src/entities/workflow-history.ts +++ b/packages/@n8n/db/src/entities/workflow-history.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm'; +import { Column, Entity, ManyToOne, PrimaryColumn, Relation } from '@n8n/typeorm'; import { IConnections } from 'n8n-workflow'; import type { INode } from 'n8n-workflow'; @@ -25,5 +25,5 @@ export class WorkflowHistory extends WithTimestamps { @ManyToOne('WorkflowEntity', { onDelete: 'CASCADE', }) - workflow: WorkflowEntity; + workflow: Relation; } diff --git a/packages/@n8n/db/src/migrations/common/1762310956274-AddWorkflowVersionIdToExecutionData.ts b/packages/@n8n/db/src/migrations/common/1762310956274-AddWorkflowVersionIdToExecutionData.ts new file mode 100644 index 00000000000..6b550e03da4 --- /dev/null +++ b/packages/@n8n/db/src/migrations/common/1762310956274-AddWorkflowVersionIdToExecutionData.ts @@ -0,0 +1,11 @@ +import type { MigrationContext, ReversibleMigration } from '../migration-types'; + +export class AddWorkflowVersionIdToExecutionData1762310956274 implements ReversibleMigration { + async up({ schemaBuilder: { addColumns, column } }: MigrationContext) { + await addColumns('execution_data', [column('workflowVersionId').varchar()]); + } + + async down({ schemaBuilder: { dropColumns } }: MigrationContext) { + await dropColumns('execution_data', ['workflowVersionId']); + } +} diff --git a/packages/@n8n/db/src/migrations/mysqldb/index.ts b/packages/@n8n/db/src/migrations/mysqldb/index.ts index b89824e42c3..f431d19803d 100644 --- a/packages/@n8n/db/src/migrations/mysqldb/index.ts +++ b/packages/@n8n/db/src/migrations/mysqldb/index.ts @@ -1,3 +1,4 @@ +import { AddWorkflowVersionIdToExecutionData1762310956274 } from './../common/1762310956274-AddWorkflowVersionIdToExecutionData'; import { DropUnusedChatHubColumns1760965142113 } from './1760965142113-DropUnusedChatHubColumns'; import { AddAudienceColumnToApiKeys1758731786132 } from '../common/1758731786132-AddAudienceColumnToApiKey'; import { CreateWorkflowDependencyTable1760314000000 } from '../common/1760314000000-CreateWorkflowDependencyTable'; @@ -221,4 +222,5 @@ export const mysqlMigrations: Migration[] = [ DropUnusedChatHubColumns1760965142113, AddWorkflowVersionColumn1761047826451, ChangeDependencyInfoToJson1761655473000, + AddWorkflowVersionIdToExecutionData1762310956274, ]; diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts index b5eeff671f6..d69263380f1 100644 --- a/packages/@n8n/db/src/migrations/postgresdb/index.ts +++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts @@ -107,6 +107,7 @@ import { CreateChatHubAgentTable1760020000000 } from '../common/1760020000000-Cr import { UniqueRoleNames1760020838000 } from '../common/1760020838000-UniqueRoleNames'; import { CreateWorkflowDependencyTable1760314000000 } from '../common/1760314000000-CreateWorkflowDependencyTable'; import { DropUnusedChatHubColumns1760965142113 } from '../common/1760965142113-DropUnusedChatHubColumns'; +import { AddWorkflowVersionIdToExecutionData1762310956274 } from '../common/1762310956274-AddWorkflowVersionIdToExecutionData'; import type { Migration } from '../migration-types'; export const postgresMigrations: Migration[] = [ @@ -219,4 +220,5 @@ export const postgresMigrations: Migration[] = [ DropUnusedChatHubColumns1760965142113, AddWorkflowVersionColumn1761047826451, ChangeDependencyInfoToJson1761655473000, + AddWorkflowVersionIdToExecutionData1762310956274, ]; diff --git a/packages/@n8n/db/src/migrations/sqlite/index.ts b/packages/@n8n/db/src/migrations/sqlite/index.ts index 94c661d0fcc..2d06a9448db 100644 --- a/packages/@n8n/db/src/migrations/sqlite/index.ts +++ b/packages/@n8n/db/src/migrations/sqlite/index.ts @@ -105,6 +105,7 @@ import { CreateChatHubTables1760019379982 } from '../common/1760019379982-Create import { CreateChatHubAgentTable1760020000000 } from '../common/1760020000000-CreateChatHubAgentTable'; import { CreateWorkflowDependencyTable1760314000000 } from '../common/1760314000000-CreateWorkflowDependencyTable'; import { DropUnusedChatHubColumns1760965142113 } from '../common/1760965142113-DropUnusedChatHubColumns'; +import { AddWorkflowVersionIdToExecutionData1762310956274 } from '../common/1762310956274-AddWorkflowVersionIdToExecutionData'; const sqliteMigrations: Migration[] = [ InitialMigration1588102412422, @@ -213,6 +214,7 @@ const sqliteMigrations: Migration[] = [ DropUnusedChatHubColumns1760965142113, AddWorkflowVersionColumn1761047826451, ChangeDependencyInfoToJson1761655473000, + AddWorkflowVersionIdToExecutionData1762310956274, ]; export { sqliteMigrations }; diff --git a/packages/@n8n/db/src/repositories/execution-data.repository.ts b/packages/@n8n/db/src/repositories/execution-data.repository.ts index 1af6a838a3e..44731d746c4 100644 --- a/packages/@n8n/db/src/repositories/execution-data.repository.ts +++ b/packages/@n8n/db/src/repositories/execution-data.repository.ts @@ -4,6 +4,8 @@ import type { EntityManager } from '@n8n/typeorm'; import type { QueryDeepPartialEntity } from '@n8n/typeorm/query-builder/QueryPartialEntity'; import { ExecutionData } from '../entities'; +import { IWorkflowBase } from 'n8n-workflow'; +import { ISimplifiedPinData } from 'entities/types-db'; @Service() export class ExecutionDataRepository extends Repository { @@ -18,12 +20,22 @@ export class ExecutionDataRepository extends Repository { return await transactionManager.insert(ExecutionData, data); } - async findByExecutionIds(executionIds: string[]) { + async findByExecutionIds(executionIds: string[]): Promise< + Array< + Omit & { + pinData?: ISimplifiedPinData; + } + > + > { return await this.find({ - select: ['workflowData'], + select: ['workflowData', 'workflowHistory'], where: { executionId: In(executionIds), }, - }).then((executionData) => executionData.map(({ workflowData }) => workflowData)); + }).then((executionData) => + executionData.map(({ workflowData, workflowHistory }) => + workflowHistory ? { ...workflowHistory.workflow, ...workflowHistory } : workflowData, + ), + ); } } diff --git a/packages/@n8n/db/src/repositories/execution.repository.ts b/packages/@n8n/db/src/repositories/execution.repository.ts index 7a9b181560c..1ac267aa50d 100644 --- a/packages/@n8n/db/src/repositories/execution.repository.ts +++ b/packages/@n8n/db/src/repositories/execution.repository.ts @@ -364,7 +364,12 @@ export class ExecutionRepository extends Repository { // In the non-pooling sqlite driver we can't use transactions, because that creates nested transactions under highly concurrent loads, leading to errors in the database const { identifiers: inserted } = await this.insert({ ...rest, createdAt: new Date() }); const { id: executionId } = inserted[0] as { id: string }; - await this.executionDataRepository.insert({ executionId, workflowData, data }); + await this.executionDataRepository.insert({ + executionId, + workflowData, + data, + workflowVersionId: currentWorkflow.versionId, + }); return String(executionId); } else { // All other database drivers should create executions and execution-data atomically @@ -375,7 +380,7 @@ export class ExecutionRepository extends Repository { }); const { id: executionId } = inserted[0] as { id: string }; await this.executionDataRepository.createExecutionDataForExecution( - { executionId, workflowData, data }, + { executionId, workflowData, data, workflowVersionId: currentWorkflow.versionId }, transactionManager, ); return String(executionId); diff --git a/packages/cli/src/modules/chat-hub/chat-hub-message.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-message.entity.ts index e66830d6988..65fb4959e2d 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-message.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-message.entity.ts @@ -10,7 +10,7 @@ import { PrimaryGeneratedColumn, } from '@n8n/typeorm'; -import type { ChatHubSession } from './chat-hub-session.entity'; +import { ChatHubSession } from './chat-hub-session.entity'; @Entity({ name: 'chat_hub_messages' }) export class ChatHubMessage extends WithTimestamps { @@ -26,7 +26,11 @@ export class ChatHubMessage extends WithTimestamps { /** * The chat session/conversation this message belongs to. */ - @ManyToOne('ChatHubSession', 'messages', { onDelete: 'CASCADE' }) + @ManyToOne( + () => ChatHubSession, + (chatHubSession) => chatHubSession.messages, + { onDelete: 'CASCADE' }, + ) @JoinColumn({ name: 'sessionId' }) session: Relation; @@ -72,7 +76,7 @@ export class ChatHubMessage extends WithTimestamps { /** * Custom n8n agent workflow that produced this message (if applicable). */ - @ManyToOne('WorkflowEntity', { onDelete: 'SET NULL', nullable: true }) + @ManyToOne(() => WorkflowEntity, { onDelete: 'SET NULL', nullable: true }) @JoinColumn({ name: 'workflowId' }) workflow?: Relation | null; @@ -92,7 +96,7 @@ export class ChatHubMessage extends WithTimestamps { /** * Execution that produced this message (reset to null when the execution is deleted) */ - @ManyToOne('ExecutionEntity', { onDelete: 'SET NULL', nullable: true }) + @ManyToOne(() => ExecutionEntity, { onDelete: 'SET NULL', nullable: true }) @JoinColumn({ name: 'executionId' }) execution?: Relation | null; @@ -105,17 +109,24 @@ export class ChatHubMessage extends WithTimestamps { /** * The previous message this message is a response to, NULL on the initial message. */ - @ManyToOne('ChatHubMessage', (m: ChatHubMessage) => m.responses, { - onDelete: 'CASCADE', - nullable: true, - }) + @ManyToOne( + () => ChatHubMessage, + (m: ChatHubMessage) => m.responses, + { + onDelete: 'CASCADE', + nullable: true, + }, + ) @JoinColumn({ name: 'previousMessageId' }) previousMessage?: Relation | null; /** * Messages that are responses to this message. This could branch out to multiple threads. */ - @OneToMany('ChatHubMessage', (m: ChatHubMessage) => m.previousMessage) + @OneToMany( + () => ChatHubMessage, + (m: ChatHubMessage) => m.previousMessage, + ) responses?: Array>; /** @@ -127,17 +138,24 @@ export class ChatHubMessage extends WithTimestamps { /** * The message that this message is a retry of (if applicable). */ - @ManyToOne('ChatHubMessage', (m: ChatHubMessage) => m.retries, { - onDelete: 'CASCADE', - nullable: true, - }) + @ManyToOne( + () => ChatHubMessage, + (m: ChatHubMessage) => m.retries, + { + onDelete: 'CASCADE', + nullable: true, + }, + ) @JoinColumn({ name: 'retryOfMessageId' }) retryOfMessage?: Relation | null; /** * All messages that are retries of this message (if applicable). */ - @OneToMany('ChatHubMessage', (m: ChatHubMessage) => m.retryOfMessage) + @OneToMany( + () => ChatHubMessage, + (m: ChatHubMessage) => m.retryOfMessage, + ) retries?: Array>; /** @@ -149,17 +167,24 @@ export class ChatHubMessage extends WithTimestamps { /** * The message that this message is a revision/edit of (if applicable). */ - @ManyToOne('ChatHubMessage', (m: ChatHubMessage) => m.revisions, { - onDelete: 'CASCADE', - nullable: true, - }) + @ManyToOne( + () => ChatHubMessage, + (m: ChatHubMessage) => m.revisions, + { + onDelete: 'CASCADE', + nullable: true, + }, + ) @JoinColumn({ name: 'revisionOfMessageId' }) revisionOfMessage?: Relation | null; /** * All messages that are revisions/edits of this message (if applicable). */ - @OneToMany('ChatHubMessage', (m: ChatHubMessage) => m.revisionOfMessage) + @OneToMany( + () => ChatHubMessage, + (m: ChatHubMessage) => m.revisionOfMessage, + ) revisions?: Array>; /**