diff --git a/packages/@n8n/db/src/entities/execution-data.ts b/packages/@n8n/db/src/entities/execution-data.ts index 9ac8251be98..a444bf07656 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 a06adadf849..0961d5bc2c9 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 44731d746c4..fdb8ddcc0ca 100644 --- a/packages/@n8n/db/src/repositories/execution-data.repository.ts +++ b/packages/@n8n/db/src/repositories/execution-data.repository.ts @@ -2,10 +2,11 @@ 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 { IWorkflowBase } from 'n8n-workflow'; + +import { ISimplifiedPinData } from 'entities/types-db'; import { ExecutionData } from '../entities'; -import { IWorkflowBase } from 'n8n-workflow'; -import { ISimplifiedPinData } from 'entities/types-db'; @Service() export class ExecutionDataRepository extends Repository { @@ -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 1ac267aa50d..7b04771b6a2 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 65fb4959e2d..e6dead49008 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 a3d12030cc2..0ecb3acb5c9 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; }