From 5cd64171f1b3f9c98cc5ed9bca9310cd4be780f7 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Wed, 5 Nov 2025 04:05:39 +0100 Subject: [PATCH 1/6] 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 3134b2b107f42..9ac8251be9863 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 cc4aa169bcff7..a06adadf849d3 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 0000000000000..6b550e03da45b --- /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 b89824e42c341..f431d19803d3c 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 b5eeff671f6ad..d69263380f108 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 94c661d0fcc5f..2d06a9448dbf1 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 1af6a838a3e42..44731d746c47b 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 7a9b181560c55..1ac267aa50dfc 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 e66830d698893..65fb4959e2d67 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>; /** From bab2f0728be8abfd58deb52bdf1b0e802d1e540d Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Wed, 5 Nov 2025 05:26:36 +0100 Subject: [PATCH 2/6] enforce constraint and offer shared accessor --- .../@n8n/db/src/entities/execution-data.ts | 41 ++++++++++++++----- .../@n8n/db/src/entities/workflow-history.ts | 2 +- .../repositories/execution-data.repository.ts | 11 ++--- .../src/repositories/execution.repository.ts | 2 +- .../chat-hub/chat-hub-message.entity.ts | 6 +-- .../chat-hub/chat-hub-session.entity.ts | 12 +++--- 6 files changed, 46 insertions(+), 28 deletions(-) diff --git a/packages/@n8n/db/src/entities/execution-data.ts b/packages/@n8n/db/src/entities/execution-data.ts index 9ac8251be9863..a444bf076567f 100644 --- a/packages/@n8n/db/src/entities/execution-data.ts +++ b/packages/@n8n/db/src/entities/execution-data.ts @@ -1,14 +1,15 @@ import { + BeforeInsert, + BeforeUpdate, Column, Entity, JoinColumn, - JoinTable, ManyToOne, OneToOne, PrimaryColumn, Relation, } from '@n8n/typeorm'; -import { IWorkflowBase } from 'n8n-workflow'; +import { IWorkflowBase, UnexpectedError } from 'n8n-workflow'; import { JsonColumn } from './abstract-entity'; import { ExecutionEntity } from './execution-entity'; @@ -16,6 +17,8 @@ import { ISimplifiedPinData } from './types-db'; import { WorkflowHistory } from './workflow-history'; import { idStringifier } from '../utils/transformers'; +type WorkflowData = Omit & { pinData?: ISimplifiedPinData }; + @Entity() export class ExecutionData { @Column('text') @@ -31,20 +34,38 @@ export class ExecutionData { * due to `INodeExecutionData`, so we use a simplified version so `QueryDeepPartialEntity` * can resolve and calls to `update`, `insert`, and `insert` pass typechecking. */ - @JsonColumn() - workflowData: Omit & { pinData?: ISimplifiedPinData }; + @JsonColumn({ nullable: true }) + workflowData: WorkflowData | null; @Column({ type: 'varchar', length: 36, nullable: true }) workflowVersionId: string | null; @ManyToOne(() => WorkflowHistory, { onDelete: 'SET NULL', nullable: true }) - @JoinTable({ - joinColumn: { - name: 'workflowVersionId', - referencedColumnName: 'versionId', - }, + @JoinColumn({ + name: 'workflowVersionId', + referencedColumnName: 'versionId', }) - workflowHistory?: Relation | null; + workflowHistory: Relation | null; + + @BeforeInsert() + @BeforeUpdate() + validateRelations() { + if (this.workflowData === null && this.workflowVersionId === null) { + throw new Error('Either workflowData or workflowVersionId must be provided'); + } + } + + get workflow(): WorkflowData { + if (this.workflowData) { + return this.workflowData; + } + + if (this.workflowHistory === null) { + throw new UnexpectedError('ExecutionData invariant broken'); + } + + return { ...this.workflowHistory.workflow, ...this.workflowHistory }; + } @PrimaryColumn({ transformer: idStringifier }) executionId: string; diff --git a/packages/@n8n/db/src/entities/workflow-history.ts b/packages/@n8n/db/src/entities/workflow-history.ts index a06adadf849d3..0961d5bc2c9e8 100644 --- a/packages/@n8n/db/src/entities/workflow-history.ts +++ b/packages/@n8n/db/src/entities/workflow-history.ts @@ -22,7 +22,7 @@ export class WorkflowHistory extends WithTimestamps { @Column() authors: string; - @ManyToOne('WorkflowEntity', { + @ManyToOne(() => WorkflowEntity, { onDelete: 'CASCADE', }) workflow: Relation; diff --git a/packages/@n8n/db/src/repositories/execution-data.repository.ts b/packages/@n8n/db/src/repositories/execution-data.repository.ts index 44731d746c47b..fdb8ddcc0ca95 100644 --- a/packages/@n8n/db/src/repositories/execution-data.repository.ts +++ b/packages/@n8n/db/src/repositories/execution-data.repository.ts @@ -2,11 +2,12 @@ import { Service } from '@n8n/di'; import { DataSource, In, Repository } from '@n8n/typeorm'; 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'; +import { ExecutionData } from '../entities'; + @Service() export class ExecutionDataRepository extends Repository { constructor(dataSource: DataSource) { @@ -32,10 +33,6 @@ export class ExecutionDataRepository extends Repository { where: { executionId: In(executionIds), }, - }).then((executionData) => - executionData.map(({ workflowData, workflowHistory }) => - workflowHistory ? { ...workflowHistory.workflow, ...workflowHistory } : workflowData, - ), - ); + }).then((executionData) => executionData.map(({ workflow }) => workflow)); } } diff --git a/packages/@n8n/db/src/repositories/execution.repository.ts b/packages/@n8n/db/src/repositories/execution.repository.ts index 1ac267aa50dfc..7b04771b6a27e 100644 --- a/packages/@n8n/db/src/repositories/execution.repository.ts +++ b/packages/@n8n/db/src/repositories/execution.repository.ts @@ -330,7 +330,7 @@ export class ExecutionRepository extends Repository { this.errorReporter.error('Found successful execution where data is empty stringified array', { extra: { executionId: execution.id, - workflowId: executionData?.workflowData.id, + workflowId: executionData?.workflow.id, }, }); } 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 65fb4959e2d67..e6dead4900885 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 @@ -127,7 +127,7 @@ export class ChatHubMessage extends WithTimestamps { () => ChatHubMessage, (m: ChatHubMessage) => m.previousMessage, ) - responses?: Array>; + responses?: Relation; /** * ID of the message that this message is a retry of (if applicable). @@ -156,7 +156,7 @@ export class ChatHubMessage extends WithTimestamps { () => ChatHubMessage, (m: ChatHubMessage) => m.retryOfMessage, ) - retries?: Array>; + retries?: Relation; /** * ID of the message that this message is a revision/edit of (if applicable). @@ -185,7 +185,7 @@ export class ChatHubMessage extends WithTimestamps { () => ChatHubMessage, (m: ChatHubMessage) => m.revisionOfMessage, ) - revisions?: Array>; + revisions?: Relation; /** * Status of the message, e.g. 'running', 'success', 'error', 'cancelled'. diff --git a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts index a3d12030cc21f..0ecb3acb5c9e3 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts @@ -10,7 +10,7 @@ import { PrimaryGeneratedColumn, } from '@n8n/typeorm'; -import type { ChatHubMessage } from './chat-hub-message.entity'; +import { ChatHubMessage } from './chat-hub-message.entity'; @Entity({ name: 'chat_hub_sessions' }) export class ChatHubSession extends WithTimestamps { @@ -33,7 +33,7 @@ export class ChatHubSession extends WithTimestamps { /** * The user that owns this chat session. */ - @ManyToOne('User', { onDelete: 'CASCADE' }) + @ManyToOne(() => User, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'ownerId' }) owner?: Relation; @@ -53,7 +53,7 @@ export class ChatHubSession extends WithTimestamps { /** * The selected credential to use by default with the selected LLM provider (if applicable). */ - @ManyToOne('CredentialsEntity', { onDelete: 'SET NULL', nullable: true }) + @ManyToOne(() => CredentialsEntity, { onDelete: 'SET NULL', nullable: true }) @JoinColumn({ name: 'credentialId' }) credential?: Relation | null; @@ -78,7 +78,7 @@ export class ChatHubSession extends WithTimestamps { /** * Custom n8n agent workflow to use (if applicable) */ - @ManyToOne('WorkflowEntity', { onDelete: 'SET NULL', nullable: true }) + @ManyToOne(() => WorkflowEntity, { onDelete: 'SET NULL', nullable: true }) @JoinColumn({ name: 'workflowId' }) workflow?: Relation | null; @@ -100,6 +100,6 @@ export class ChatHubSession extends WithTimestamps { /** * All messages that belong to this chat session. */ - @OneToMany('ChatHubMessage', 'session') - messages?: Array>; + @OneToMany(() => ChatHubMessage, 'session') + messages?: Relation; } From cb97ba13528a63d0560875c05c8de75c3ea6ec8c Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 13 Nov 2025 11:15:54 +0100 Subject: [PATCH 3/6] fix build --- packages/@n8n/db/src/entities/workflow-entity.ts | 8 ++++---- packages/@n8n/db/src/entities/workflow-statistics.ts | 4 ++-- .../cli/src/modules/chat-hub/chat-hub-agent.entity.ts | 2 +- .../cli/src/modules/chat-hub/chat-hub-session.entity.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@n8n/db/src/entities/workflow-entity.ts b/packages/@n8n/db/src/entities/workflow-entity.ts index 026a0efeb1274..bb9d27fb2d237 100644 --- a/packages/@n8n/db/src/entities/workflow-entity.ts +++ b/packages/@n8n/db/src/entities/workflow-entity.ts @@ -15,11 +15,11 @@ import type { INode } from 'n8n-workflow'; import { JsonColumn, WithTimestampsAndStringId, dbType } from './abstract-entity'; import { type Folder } from './folder'; import type { SharedWorkflow } from './shared-workflow'; -import type { TagEntity } from './tag-entity'; +import { TagEntity } from './tag-entity'; import type { TestRun } from './test-run.ee'; import type { ISimplifiedPinData, IWorkflowDb } from './types-db'; import type { WorkflowStatistics } from './workflow-statistics'; -import type { WorkflowTagMapping } from './workflow-tag-mapping'; +import { WorkflowTagMapping } from './workflow-tag-mapping'; import { objectRetriever, sqlite } from '../utils/transformers'; @Entity() @@ -69,7 +69,7 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl }) meta?: WorkflowFEMeta; - @ManyToMany('TagEntity', 'workflows') + @ManyToMany(() => TagEntity, 'workflows') @JoinTable({ name: 'workflows_tags', // table name for the junction table of this relation joinColumn: { @@ -83,7 +83,7 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl }) tags?: TagEntity[]; - @OneToMany('WorkflowTagMapping', 'workflows') + @OneToMany(() => WorkflowTagMapping, 'workflows') tagMappings: WorkflowTagMapping[]; @OneToMany('SharedWorkflow', 'workflow') diff --git a/packages/@n8n/db/src/entities/workflow-statistics.ts b/packages/@n8n/db/src/entities/workflow-statistics.ts index e189462d4221a..c8fab46968a9f 100644 --- a/packages/@n8n/db/src/entities/workflow-statistics.ts +++ b/packages/@n8n/db/src/entities/workflow-statistics.ts @@ -1,7 +1,7 @@ import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm'; import { DateTimeColumn } from './abstract-entity'; -import { StatisticsNames } from './types-db'; +import type { StatisticsNames } from './types-db'; import { WorkflowEntity } from './workflow-entity'; @Entity() @@ -18,7 +18,7 @@ export class WorkflowStatistics { @PrimaryColumn({ length: 128 }) name: StatisticsNames; - @ManyToOne('WorkflowEntity', 'shared') + @ManyToOne(() => WorkflowEntity, 'shared') workflow: WorkflowEntity; @PrimaryColumn() diff --git a/packages/cli/src/modules/chat-hub/chat-hub-agent.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-agent.entity.ts index e47972b9ab54a..b860ebeee7b8c 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-agent.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-agent.entity.ts @@ -47,7 +47,7 @@ export class ChatHubAgent extends WithTimestamps { /** * The selected credential to use by default with the selected LLM provider (if applicable). */ - @ManyToOne('CredentialsEntity', { onDelete: 'SET NULL', nullable: true }) + @ManyToOne(() => CredentialsEntity, { onDelete: 'SET NULL', nullable: true }) @JoinColumn({ name: 'credentialId' }) credential?: CredentialsEntity | null; diff --git a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts index 0ecb3acb5c9e3..bd9c923ce6456 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-session.entity.ts @@ -10,7 +10,7 @@ import { PrimaryGeneratedColumn, } from '@n8n/typeorm'; -import { ChatHubMessage } from './chat-hub-message.entity'; +import type { ChatHubMessage } from './chat-hub-message.entity'; @Entity({ name: 'chat_hub_sessions' }) export class ChatHubSession extends WithTimestamps { @@ -100,6 +100,6 @@ export class ChatHubSession extends WithTimestamps { /** * All messages that belong to this chat session. */ - @OneToMany(() => ChatHubMessage, 'session') + @OneToMany('ChatHubMessage', 'session') messages?: Relation; } From 5bdfb4ae2f263f724dc7cc2642ac1ef8479a7517 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 13 Nov 2025 14:22:00 +0100 Subject: [PATCH 4/6] Add the actual fix --- packages/@n8n/db/src/entities/execution-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@n8n/db/src/entities/execution-data.ts b/packages/@n8n/db/src/entities/execution-data.ts index a444bf076567f..6d3124ee359aa 100644 --- a/packages/@n8n/db/src/entities/execution-data.ts +++ b/packages/@n8n/db/src/entities/execution-data.ts @@ -45,7 +45,7 @@ export class ExecutionData { name: 'workflowVersionId', referencedColumnName: 'versionId', }) - workflowHistory: Relation | null; + workflowHistory: Relation & { workflow: WorkflowData }> | null; // this @BeforeInsert() @BeforeUpdate() From ca54c0020e527017944816272ce80f744a3b6ba9 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 13 Nov 2025 14:43:01 +0100 Subject: [PATCH 5/6] Add Relation --- packages/@n8n/db/src/entities/execution-data.ts | 3 ++- packages/@n8n/db/src/entities/shared-workflow.ts | 4 ++-- packages/@n8n/db/src/entities/workflow-entity.ts | 3 ++- packages/@n8n/db/src/entities/workflow-history.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/@n8n/db/src/entities/execution-data.ts b/packages/@n8n/db/src/entities/execution-data.ts index 6d3124ee359aa..d86c6541399ae 100644 --- a/packages/@n8n/db/src/entities/execution-data.ts +++ b/packages/@n8n/db/src/entities/execution-data.ts @@ -45,7 +45,8 @@ export class ExecutionData { name: 'workflowVersionId', referencedColumnName: 'versionId', }) - workflowHistory: Relation & { workflow: WorkflowData }> | null; // this + workflowHistory: Relation | null; // this + // workflowHistory: Relation & { workflow: WorkflowData }> | null; // this @BeforeInsert() @BeforeUpdate() diff --git a/packages/@n8n/db/src/entities/shared-workflow.ts b/packages/@n8n/db/src/entities/shared-workflow.ts index 147454f60e516..944524957949b 100644 --- a/packages/@n8n/db/src/entities/shared-workflow.ts +++ b/packages/@n8n/db/src/entities/shared-workflow.ts @@ -1,5 +1,5 @@ import { WorkflowSharingRole } from '@n8n/permissions'; -import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm'; +import { Column, Entity, ManyToOne, PrimaryColumn, Relation } from '@n8n/typeorm'; import { WithTimestamps } from './abstract-entity'; import { Project } from './project'; @@ -11,7 +11,7 @@ export class SharedWorkflow extends WithTimestamps { role: WorkflowSharingRole; @ManyToOne('WorkflowEntity', 'shared') - workflow: WorkflowEntity; + workflow: Relation; @PrimaryColumn() workflowId: string; diff --git a/packages/@n8n/db/src/entities/workflow-entity.ts b/packages/@n8n/db/src/entities/workflow-entity.ts index bb9d27fb2d237..b86bc4ef659ec 100644 --- a/packages/@n8n/db/src/entities/workflow-entity.ts +++ b/packages/@n8n/db/src/entities/workflow-entity.ts @@ -7,6 +7,7 @@ import { ManyToMany, ManyToOne, OneToMany, + Relation, } from '@n8n/typeorm'; import { Length } from 'class-validator'; import { IConnections, IDataObject, IWorkflowSettings, WorkflowFEMeta } from 'n8n-workflow'; @@ -87,7 +88,7 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl tagMappings: WorkflowTagMapping[]; @OneToMany('SharedWorkflow', 'workflow') - shared: SharedWorkflow[]; + shared: Relation; @OneToMany('WorkflowStatistics', 'workflow') @JoinColumn({ referencedColumnName: 'workflow' }) diff --git a/packages/@n8n/db/src/entities/workflow-history.ts b/packages/@n8n/db/src/entities/workflow-history.ts index 0961d5bc2c9e8..a50d7f5064a08 100644 --- a/packages/@n8n/db/src/entities/workflow-history.ts +++ b/packages/@n8n/db/src/entities/workflow-history.ts @@ -25,5 +25,5 @@ export class WorkflowHistory extends WithTimestamps { @ManyToOne(() => WorkflowEntity, { onDelete: 'CASCADE', }) - workflow: Relation; + workflow: Relation; } From d24607f43524b848c508b3d2c9c0898b68342dd9 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 13 Nov 2025 15:59:52 +0100 Subject: [PATCH 6/6] compiling code --- packages/@n8n/db/src/entities/execution-data.ts | 13 ++++++------- packages/@n8n/db/src/entities/shared-workflow.ts | 5 ++++- packages/@n8n/db/src/entities/workflow-history.ts | 8 ++++++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/@n8n/db/src/entities/execution-data.ts b/packages/@n8n/db/src/entities/execution-data.ts index d86c6541399ae..7133a7d2a8133 100644 --- a/packages/@n8n/db/src/entities/execution-data.ts +++ b/packages/@n8n/db/src/entities/execution-data.ts @@ -40,13 +40,12 @@ export class ExecutionData { @Column({ type: 'varchar', length: 36, nullable: true }) workflowVersionId: string | null; - @ManyToOne(() => WorkflowHistory, { onDelete: 'SET NULL', nullable: true }) - @JoinColumn({ - name: 'workflowVersionId', - referencedColumnName: 'versionId', - }) - workflowHistory: Relation | null; // this - // workflowHistory: Relation & { workflow: WorkflowData }> | null; // this + @ManyToOne( + () => WorkflowHistory, + (wh) => wh.versionId, + { onDelete: 'SET NULL', nullable: true }, + ) + workflowHistory: (Omit & { workflow: WorkflowData }) | null; @BeforeInsert() @BeforeUpdate() diff --git a/packages/@n8n/db/src/entities/shared-workflow.ts b/packages/@n8n/db/src/entities/shared-workflow.ts index 944524957949b..d8648fc7f3ff3 100644 --- a/packages/@n8n/db/src/entities/shared-workflow.ts +++ b/packages/@n8n/db/src/entities/shared-workflow.ts @@ -10,7 +10,10 @@ export class SharedWorkflow extends WithTimestamps { @Column({ type: 'varchar' }) role: WorkflowSharingRole; - @ManyToOne('WorkflowEntity', 'shared') + @ManyToOne( + () => WorkflowEntity, + (we) => we.shared, + ) workflow: Relation; @PrimaryColumn() diff --git a/packages/@n8n/db/src/entities/workflow-history.ts b/packages/@n8n/db/src/entities/workflow-history.ts index a50d7f5064a08..dc0170d798e93 100644 --- a/packages/@n8n/db/src/entities/workflow-history.ts +++ b/packages/@n8n/db/src/entities/workflow-history.ts @@ -1,8 +1,9 @@ -import { Column, Entity, ManyToOne, PrimaryColumn, Relation } from '@n8n/typeorm'; +import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn, Relation } from '@n8n/typeorm'; import { IConnections } from 'n8n-workflow'; import type { INode } from 'n8n-workflow'; import { JsonColumn, WithTimestamps } from './abstract-entity'; +import type { ExecutionData } from './execution-data'; import { WorkflowEntity } from './workflow-entity'; @Entity() @@ -25,5 +26,8 @@ export class WorkflowHistory extends WithTimestamps { @ManyToOne(() => WorkflowEntity, { onDelete: 'CASCADE', }) - workflow: Relation; + workflow: Relation; + + @OneToMany('ExecutionData', 'workflowVersionId') + executionData: Relation; }