enforce constraint and offer shared accessor

This commit is contained in:
Charlie Kolb 2025-11-05 05:26:36 +01:00
parent 5cd64171f1
commit bab2f0728b
No known key found for this signature in database
6 changed files with 46 additions and 28 deletions

View File

@ -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<IWorkflowBase, 'pinData'> & { 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<IWorkflowBase, 'pinData'> & { 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<WorkflowHistory> | null;
workflowHistory: Relation<WorkflowHistory> | 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;

View File

@ -22,7 +22,7 @@ export class WorkflowHistory extends WithTimestamps {
@Column()
authors: string;
@ManyToOne('WorkflowEntity', {
@ManyToOne(() => WorkflowEntity, {
onDelete: 'CASCADE',
})
workflow: Relation<WorkflowEntity>;

View File

@ -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<ExecutionData> {
@ -32,10 +33,6 @@ export class ExecutionDataRepository extends Repository<ExecutionData> {
where: {
executionId: In(executionIds),
},
}).then((executionData) =>
executionData.map(({ workflowData, workflowHistory }) =>
workflowHistory ? { ...workflowHistory.workflow, ...workflowHistory } : workflowData,
),
);
}).then((executionData) => executionData.map(({ workflow }) => workflow));
}
}

View File

@ -330,7 +330,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
this.errorReporter.error('Found successful execution where data is empty stringified array', {
extra: {
executionId: execution.id,
workflowId: executionData?.workflowData.id,
workflowId: executionData?.workflow.id,
},
});
}

View File

@ -127,7 +127,7 @@ export class ChatHubMessage extends WithTimestamps {
() => ChatHubMessage,
(m: ChatHubMessage) => m.previousMessage,
)
responses?: Array<Relation<ChatHubMessage>>;
responses?: Relation<ChatHubMessage[]>;
/**
* 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<Relation<ChatHubMessage>>;
retries?: Relation<ChatHubMessage[]>;
/**
* 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<Relation<ChatHubMessage>>;
revisions?: Relation<ChatHubMessage[]>;
/**
* Status of the message, e.g. 'running', 'success', 'error', 'cancelled'.

View File

@ -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<User>;
@ -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<CredentialsEntity> | 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<WorkflowEntity> | null;
@ -100,6 +100,6 @@ export class ChatHubSession extends WithTimestamps {
/**
* All messages that belong to this chat session.
*/
@OneToMany('ChatHubMessage', 'session')
messages?: Array<Relation<ChatHubMessage>>;
@OneToMany(() => ChatHubMessage, 'session')
messages?: Relation<ChatHubMessage[]>;
}