Skip to content

Commit f0883c1

Browse files
CharlieKolbdariacodes
authored andcommitted
Add workflow publish history
1 parent ca84de8 commit f0883c1

File tree

18 files changed

+230
-33
lines changed

18 files changed

+230
-33
lines changed

packages/@n8n/db/src/entities/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { WebhookEntity } from './webhook-entity';
2929
import { WorkflowDependency } from './workflow-dependency-entity';
3030
import { WorkflowEntity } from './workflow-entity';
3131
import { WorkflowHistory } from './workflow-history';
32+
import { WorkflowPublishHistory } from './workflow-publish-history';
3233
import { WorkflowStatistics } from './workflow-statistics';
3334
import { WorkflowTagMapping } from './workflow-tag-mapping';
3435

@@ -58,6 +59,7 @@ export {
5859
FolderTagMapping,
5960
AuthProviderSyncHistory,
6061
WorkflowHistory,
62+
WorkflowPublishHistory,
6163
ExecutionData,
6264
ExecutionMetadata,
6365
AnnotationTagEntity,
@@ -93,6 +95,7 @@ export const entities = {
9395
FolderTagMapping,
9496
AuthProviderSyncHistory,
9597
WorkflowHistory,
98+
WorkflowPublishHistory,
9699
ExecutionData,
97100
ExecutionMetadata,
98101
AnnotationTagEntity,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn } from '@n8n/typeorm';
2+
import { WorkflowActivateMode } from 'n8n-workflow';
3+
4+
import { WithCreatedAt } from './abstract-entity';
5+
import { User } from './user';
6+
7+
@Entity()
8+
@Index(['workflowId', 'versionId'])
9+
export class WorkflowPublishHistory extends WithCreatedAt {
10+
@PrimaryGeneratedColumn()
11+
id: number;
12+
13+
@Column({ type: 'varchar' })
14+
@Index()
15+
workflowId: string;
16+
17+
@Column({ type: 'varchar' })
18+
versionId: string | null;
19+
20+
@Column()
21+
status: 'activated' | 'deactivated';
22+
23+
// We only expect 'activate', 'update' and 'init' from WorkflowActivateMode
24+
// But this makes usage wrt typings easier.
25+
// If you ever see other values here this would be unexpected though
26+
@Column({ type: 'varchar' })
27+
mode: WorkflowActivateMode | 'deactivate' | null;
28+
29+
@Column({ type: 'varchar' })
30+
userId: string | null;
31+
32+
@OneToOne('User', 'id', {
33+
onDelete: 'SET NULL',
34+
})
35+
@JoinColumn({ name: 'userId' })
36+
user: User;
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { MigrationContext, ReversibleMigration } from '../migration-types';
2+
3+
const workflowPublishHistoryTableName = 'workflow_publish_history';
4+
5+
export class CreateWorkflowPublishHistoryTable1763387043735 implements ReversibleMigration {
6+
async up({ schemaBuilder: { createTable, column } }: MigrationContext) {
7+
await createTable(workflowPublishHistoryTableName)
8+
.withColumns(
9+
column('id').int.primary.autoGenerate2,
10+
column('workflowId').varchar(36).notNull,
11+
column('versionId').varchar(36),
12+
column('status').varchar(36).notNull,
13+
column('mode').varchar(36),
14+
column('userId').varchar(36),
15+
)
16+
.withCreatedAt.withForeignKey('workflowId', {
17+
tableName: 'workflow_entity',
18+
columnName: 'id',
19+
onDelete: 'CASCADE',
20+
})
21+
.withForeignKey('versionId', {
22+
tableName: 'workflow_history',
23+
columnName: 'versionId',
24+
onDelete: 'SET NULL',
25+
})
26+
.withForeignKey('userId', {
27+
tableName: 'user',
28+
columnName: 'id',
29+
onDelete: 'SET NULL',
30+
})
31+
.withIndexOn(['workflowId', 'versionId']);
32+
}
33+
34+
async down({ schemaBuilder: { dropTable } }: MigrationContext) {
35+
await dropTable(workflowPublishHistoryTableName);
36+
}
37+
}

packages/@n8n/db/src/migrations/mysqldb/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/17
116116
import { AddIsGlobalColumnToCredentialsTable1762771954619 } from '../common/1762771954619-IsGlobalGlobalColumnToCredentialsTable';
117117
import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields';
118118
import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn';
119+
import { CreateWorkflowPublishHistoryTable1763387043735 } from '../common/1763387043735-CreateWorkflowPublishHistoryTable';
119120
import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar';
120121
import { CreateBinaryDataTable1763716655000 } from '../common/1763716655000-CreateBinaryDataTable';
121122
import type { Migration } from '../migration-types';
@@ -241,4 +242,5 @@ export const mysqlMigrations: Migration[] = [
241242
AddAttachmentsToChatHubMessages1761773155024,
242243
AddActiveVersionIdColumn1763047800000,
243244
CreateBinaryDataTable1763716655000,
245+
CreateWorkflowPublishHistoryTable1763387043735,
244246
];

packages/@n8n/db/src/migrations/postgresdb/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/17
116116
import { AddIsGlobalColumnToCredentialsTable1762771954619 } from '../common/1762771954619-IsGlobalGlobalColumnToCredentialsTable';
117117
import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields';
118118
import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn';
119+
import { CreateWorkflowPublishHistoryTable1763387043735 } from '../common/1763387043735-CreateWorkflowPublishHistoryTable';
119120
import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar';
120121
import { CreateBinaryDataTable1763716655000 } from '../common/1763716655000-CreateBinaryDataTable';
121122
import type { Migration } from '../migration-types';
@@ -241,4 +242,5 @@ export const postgresMigrations: Migration[] = [
241242
AddAttachmentsToChatHubMessages1761773155024,
242243
AddActiveVersionIdColumn1763047800000,
243244
CreateBinaryDataTable1763716655000,
245+
CreateWorkflowPublishHistoryTable1763387043735,
244246
];

packages/@n8n/db/src/migrations/sqlite/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/17
112112
import { AddIsGlobalColumnToCredentialsTable1762771954619 } from '../common/1762771954619-IsGlobalGlobalColumnToCredentialsTable';
113113
import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields';
114114
import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn';
115+
import { CreateWorkflowPublishHistoryTable1763387043735 } from '../common/1763387043735-CreateWorkflowPublishHistoryTable';
115116
import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar';
116117
import { CreateBinaryDataTable1763716655000 } from '../common/1763716655000-CreateBinaryDataTable';
117118
import type { Migration } from '../migration-types';
@@ -233,6 +234,7 @@ const sqliteMigrations: Migration[] = [
233234
AddAttachmentsToChatHubMessages1761773155024,
234235
AddActiveVersionIdColumn1763047800000,
235236
CreateBinaryDataTable1763716655000,
237+
CreateWorkflowPublishHistoryTable1763387043735,
236238
];
237239

238240
export { sqliteMigrations };

packages/@n8n/db/src/repositories/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export { WorkflowTagMappingRepository } from './workflow-tag-mapping.repository'
2929
export { SharedWorkflowRepository } from './shared-workflow.repository';
3030
export { SharedCredentialsRepository } from './shared-credentials.repository';
3131
export { WorkflowRepository } from './workflow.repository';
32+
export { WorkflowPublishHistoryRepository } from './workflow-publish-history.repository';
3233
export {
3334
WorkflowDependencyRepository,
3435
WorkflowDependencies,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Service } from '@n8n/di';
2+
import { DataSource, Equal, Or, Repository } from '@n8n/typeorm';
3+
4+
import { WorkflowPublishHistory } from '../entities';
5+
6+
@Service()
7+
export class WorkflowPublishHistoryRepository extends Repository<WorkflowPublishHistory> {
8+
constructor(dataSource: DataSource) {
9+
super(WorkflowPublishHistory, dataSource.manager);
10+
}
11+
12+
async addRecord({
13+
workflowId,
14+
versionId,
15+
status,
16+
mode,
17+
userId,
18+
}: Pick<WorkflowPublishHistory, 'status' | 'workflowId' | 'versionId' | 'mode' | 'userId'>) {
19+
return await this.insert({
20+
workflowId,
21+
versionId,
22+
status,
23+
mode,
24+
userId,
25+
});
26+
}
27+
28+
async getLastActivatedVersion(workflowId: string) {
29+
return await this.findOne({
30+
select: ['versionId'],
31+
where: {
32+
workflowId,
33+
status: 'activated',
34+
mode: 'activate',
35+
},
36+
order: { createdAt: 'DESC' },
37+
});
38+
}
39+
40+
async getPublishedVersions(workflowId: string, includeUser?: boolean) {
41+
const select: Array<keyof WorkflowPublishHistory> = ['versionId', 'createdAt'];
42+
if (includeUser) {
43+
select.push('user');
44+
}
45+
const result = await this.find({
46+
select,
47+
where: {
48+
workflowId,
49+
status: 'activated',
50+
mode: Or(Equal('activate'), Equal('update')),
51+
},
52+
});
53+
54+
return result;
55+
}
56+
}

packages/cli/src/__tests__/active-workflow-manager.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('ActiveWorkflowManager', () => {
3636
mock(),
3737
mock(),
3838
mock(),
39+
mock(),
3940
instanceSettings,
4041
mock(),
4142
mock(),

packages/cli/src/active-workflow-manager.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { Logger } from '@n8n/backend-common';
88
import { WorkflowsConfig } from '@n8n/config';
99
import type { WorkflowEntity, IWorkflowDb } from '@n8n/db';
10-
import { WorkflowRepository } from '@n8n/db';
10+
import { WorkflowRepository, WorkflowPublishHistoryRepository } from '@n8n/db';
1111
import { OnLeaderStepdown, OnLeaderTakeover, OnPubSubEvent, OnShutdown } from '@n8n/decorators';
1212
import { Service } from '@n8n/di';
1313
import chunk from 'lodash/chunk';
@@ -59,6 +59,8 @@ import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-da
5959
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
6060
import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service';
6161
import { formatWorkflow } from '@/workflows/workflow.formatter';
62+
import { PubSubCommandMap } from './scaling/pubsub/pubsub.event-map';
63+
import { User } from '@n8n/api-types';
6264

6365
interface QueuedActivation {
6466
activationMode: WorkflowActivateMode;
@@ -80,6 +82,7 @@ export class ActiveWorkflowManager {
8082
private readonly nodeTypes: NodeTypes,
8183
private readonly webhookService: WebhookService,
8284
private readonly workflowRepository: WorkflowRepository,
85+
private readonly workflowPublishHistoryRepository: WorkflowPublishHistoryRepository,
8386
private readonly activationErrorsService: ActivationErrorsService,
8487
private readonly executionService: ExecutionService,
8588
private readonly workflowStaticDataService: WorkflowStaticDataService,
@@ -561,6 +564,7 @@ export class ActiveWorkflowManager {
561564
activationMode: WorkflowActivateMode,
562565
existingWorkflow?: WorkflowEntity,
563566
{ shouldPublish } = { shouldPublish: true },
567+
userId: string | null = null,
564568
) {
565569
const added = { webhooks: false, triggersAndPollers: false };
566570

@@ -655,6 +659,13 @@ export class ActiveWorkflowManager {
655659

656660
const triggerCount = this.countTriggers(workflow, additionalData);
657661
await this.workflowRepository.updateWorkflowTriggerCount(workflow.id, triggerCount);
662+
await this.workflowPublishHistoryRepository.addRecord({
663+
workflowId,
664+
versionId: dbWorkflow.versionId,
665+
status: 'activated',
666+
mode: activationMode,
667+
userId,
668+
});
658669
} catch (e) {
659670
const error = e instanceof Error ? e : new Error(`${e}`);
660671
await this.activationErrorsService.register(workflowId, error.message);
@@ -670,7 +681,7 @@ export class ActiveWorkflowManager {
670681
}
671682

672683
@OnPubSubEvent('display-workflow-activation', { instanceType: 'main' })
673-
handleDisplayWorkflowActivation({ workflowId }: { workflowId: string }) {
684+
handleDisplayWorkflowActivation({ workflowId }: PubSubCommandMap['display-workflow-activation']) {
674685
this.push.broadcast({ type: 'workflowActivated', data: { workflowId } });
675686
}
676687

@@ -697,7 +708,9 @@ export class ActiveWorkflowManager {
697708
instanceType: 'main',
698709
instanceRole: 'leader',
699710
})
700-
async handleAddWebhooksTriggersAndPollers({ workflowId }: { workflowId: string }) {
711+
async handleAddWebhooksTriggersAndPollers({
712+
workflowId,
713+
}: PubSubCommandMap['add-webhooks-triggers-and-pollers']) {
701714
try {
702715
await this.add(workflowId, 'activate', undefined, {
703716
shouldPublish: false, // prevent leader from re-publishing message
@@ -881,7 +894,7 @@ export class ActiveWorkflowManager {
881894
*/
882895
// TODO: this should happen in a transaction
883896
// maybe, see: https://github.com/n8n-io/n8n/pull/8904#discussion_r1530150510
884-
async remove(workflowId: WorkflowId) {
897+
async remove(workflowId: WorkflowId, userId?: User['id'], reason?: 'update' | 'deactivate') {
885898
if (this.instanceSettings.isMultiMain) {
886899
try {
887900
await this.clearWebhooks(workflowId);
@@ -918,6 +931,14 @@ export class ActiveWorkflowManager {
918931
// if it's active in memory then it's a trigger
919932
// so remove from list of actives workflows
920933
await this.removeWorkflowTriggersAndPollers(workflowId);
934+
935+
await this.workflowPublishHistoryRepository.addRecord({
936+
workflowId,
937+
versionId: null,
938+
status: 'deactivated',
939+
mode: reason ?? null,
940+
userId: userId ?? null,
941+
});
921942
}
922943

923944
@OnPubSubEvent('remove-triggers-and-pollers', { instanceType: 'main', instanceRole: 'leader' })

0 commit comments

Comments
 (0)