Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
c901a9b
Add activeVersionId to workflow entity
dariacodes Oct 24, 2025
8e956c4
Refactor activate workflow logic to use active version
dariacodes Oct 26, 2025
ee7ddff
Fix activateAll and deactivateAll CLI commands
dariacodes Oct 26, 2025
bbc3af0
Fix migration to only use existing version ids
dariacodes Oct 26, 2025
343299d
Add tests for the public endpoint, account for active being read-only
dariacodes Oct 26, 2025
232d38e
Add tests for internal update endpoint
dariacodes Oct 26, 2025
194b5fa
Fix executeErrorWorkflow to use active version on activation failure
dariacodes Oct 27, 2025
950d726
Fix production webhooks to execute active version instead of draft
dariacodes Oct 27, 2025
20e35f2
Update migration to not check for version existence anymore (all user…
dariacodes Oct 29, 2025
69120a7
Remove fallbacks to current version
dariacodes Oct 29, 2025
1bfc817
Merge branch 'master' into ado-4277-refactor
dariacodes Oct 29, 2025
346a926
Roll back versionId change for V1 activate
dariacodes Nov 10, 2025
446cc96
Update migration timestamp
dariacodes Nov 10, 2025
b3b1c7a
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 10, 2025
48fd32c
Add activeVersion object to response of getWorkflow(s) V1
dariacodes Nov 10, 2025
d73e641
Remove tests with versionId (will be moved to V2)
dariacodes Nov 10, 2025
0ba99da
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 11, 2025
3b9de5c
Fix import
dariacodes Nov 11, 2025
b421d38
Remove WF history license artifacts from tests
dariacodes Nov 11, 2025
43322c1
Update active version on PATCH only when needed
dariacodes Nov 11, 2025
8e67761
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 11, 2025
2fdc885
Improve tests
dariacodes Nov 11, 2025
7c35aa0
Fix import order
dariacodes Nov 11, 2025
f66b162
Remove an obsolete check
dariacodes Nov 11, 2025
9625aec
Remove duplicate tests
dariacodes Nov 11, 2025
e6907e7
Add missing relation
dariacodes Nov 11, 2025
2736a89
Fix api schema
dariacodes Nov 11, 2025
f84d1f4
Move active version flags to ref for api schema
dariacodes Nov 11, 2025
fb1c30f
Set active version on workflow creation (e.g. through import)
dariacodes Nov 11, 2025
3d3b70d
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 11, 2025
c5f374f
Update migration to never remove active versions from workflow history
dariacodes Nov 13, 2025
5a44c1d
Add activeVersionId to workflow entity, update queries
dariacodes Nov 13, 2025
8269116
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 13, 2025
dc802f1
Fix entity truncate order in tests
dariacodes Nov 13, 2025
6f9130c
Update pruning logic to not delete active versions as well
dariacodes Nov 13, 2025
7939fb0
Improve cli workflow update commands tests
dariacodes Nov 13, 2025
a4dc093
Improve workflow repository tests
dariacodes Nov 13, 2025
acb8fcf
Move version saving up during workflow creation
dariacodes Nov 13, 2025
8b3afb7
Attempt to solve foreign key circular dependencies for MySQL and MariaDB
dariacodes Nov 13, 2025
cea6e0f
Fix quotes in activateAll query
dariacodes Nov 13, 2025
56f92c0
Add more truncation in tests
dariacodes Nov 13, 2025
56c30cc
Truncate junction tables
dariacodes Nov 13, 2025
0368bdc
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 14, 2025
5bea05f
Use activeVersionId on backend where active is used
dariacodes Nov 18, 2025
f0ef29a
Use activeVersionId on frontend where active is used
dariacodes Nov 18, 2025
c2361e7
Update tests
dariacodes Nov 18, 2025
e4f0ff1
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 18, 2025
1f86751
Fix lint issue
dariacodes Nov 18, 2025
123db55
Fix mcp tests
dariacodes Nov 18, 2025
110e5ae
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 18, 2025
3760bcb
Fix roll-back value
dariacodes Nov 18, 2025
92ab28c
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 20, 2025
dc154a3
Make the check in job processor more explicit
dariacodes Nov 20, 2025
0f8bcce
Use only activeVersionId in source control import
dariacodes Nov 20, 2025
41062b7
Return correct active status for execution workflow data
dariacodes Nov 20, 2025
fe653e7
Improve checks for active workflow manager
dariacodes Nov 20, 2025
b02abf0
Fix test active workflow manager test mocks
dariacodes Nov 20, 2025
1c4975b
Remove activeVersionId from WorkflowDataUpdate
dariacodes Nov 20, 2025
fd139f9
Use the right workflow version in execute additional data
dariacodes Nov 20, 2025
2e5a093
Remove activeVersionId filter from useWorkflowHelpers
dariacodes Nov 20, 2025
e6b74ca
Fix comments
dariacodes Nov 20, 2025
aa8a29f
Merge branch 'master' into ado-4277-refactor
dariacodes Nov 20, 2025
4721f7e
Add a test checking that current version is used for manual executions
dariacodes Nov 20, 2025
fe34ed7
Rename util
dariacodes Nov 20, 2025
ba03d02
Update db workflow repo test
dariacodes Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 84 additions & 6 deletions packages/@n8n/backend-test-utils/src/db/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ export async function createManyWorkflows(
return await Promise.all(workflowRequests);
}

export async function createManyActiveWorkflows(
amount: number,
attributes: Partial<IWorkflowDb> = {},
userOrProject?: User | Project,
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const workflowRequests = [...Array(amount)].map(
async (_) => await createActiveWorkflow(attributes, userOrProject),
);
return await Promise.all(workflowRequests);
}

export async function shareWorkflowWithUsers(workflow: IWorkflowBase, users: User[]) {
const sharedWorkflows: Array<DeepPartial<SharedWorkflow>> = await Promise.all(
users.map(async (user) => {
Expand Down Expand Up @@ -135,7 +147,7 @@ export async function getWorkflowSharing(workflow: IWorkflowBase) {
*/
export async function createWorkflowWithTrigger(
attributes: Partial<IWorkflowDb> = {},
user?: User,
userOrProject?: User | Project,
) {
const workflow = await createWorkflow(
{
Expand Down Expand Up @@ -170,7 +182,7 @@ export async function createWorkflowWithTrigger(
},
...attributes,
},
user,
userOrProject,
);

return workflow;
Expand Down Expand Up @@ -201,12 +213,12 @@ export async function createWorkflowWithHistory(
*/
export async function createWorkflowWithTriggerAndHistory(
attributes: Partial<IWorkflowDb> = {},
user?: User,
userOrProject?: User | Project,
) {
const workflow = await createWorkflowWithTrigger(attributes, user);
const workflow = await createWorkflowWithTrigger(attributes, userOrProject);

// Create workflow history for the initial version
await createWorkflowHistory(workflow, user);
await createWorkflowHistory(workflow, userOrProject);

return workflow;
}
Expand All @@ -227,12 +239,78 @@ export const getWorkflowById = async (id: string) =>
* @param workflow workflow to create history for
* @param user user who created the version (optional)
*/
export async function createWorkflowHistory(workflow: IWorkflowDb, user?: User): Promise<void> {
export async function createWorkflowHistory(
workflow: IWorkflowDb,
userOrProject?: User | Project,
): Promise<void> {
await Container.get(WorkflowHistoryRepository).insert({
workflowId: workflow.id,
versionId: workflow.versionId,
nodes: workflow.nodes,
connections: workflow.connections,
authors: userOrProject instanceof User ? userOrProject.email : '[email protected]',
});
}

/**
* Set the active version for a workflow
* @param workflowId workflow ID
* @param versionId version ID to set as active
*/
export async function setActiveVersion(workflowId: string, versionId: string): Promise<void> {
await Container.get(WorkflowRepository)
.createQueryBuilder()
.update()
.set({ activeVersionId: versionId })
.where('id = :workflowId', { workflowId })
.execute();
}

/**
* Create an active workflow with trigger, history, and activeVersionId set to the current version.
* This simulates a workflow that has been activated and is running.
* @param attributes workflow attributes
* @param user user to assign the workflow to
*/
export async function createActiveWorkflow(
attributes: Partial<IWorkflowDb> = {},
userOrProject?: User | Project,
) {
const workflow = await createWorkflowWithTriggerAndHistory(
{ active: true, ...attributes },
userOrProject,
);

await setActiveVersion(workflow.id, workflow.versionId);

workflow.activeVersionId = workflow.versionId;
return workflow;
}

/**
* Create a workflow with a specific active version.
* This simulates a workflow where the active version differs from the current version.
* @param activeVersionId the version ID to set as active
* @param attributes workflow attributes
* @param user user to assign the workflow to
*/
export async function createWorkflowWithActiveVersion(
activeVersionId: string,
attributes: Partial<IWorkflowDb> = {},
user?: User,
) {
const workflow = await createWorkflowWithTriggerAndHistory({ active: true, ...attributes }, user);

await Container.get(WorkflowHistoryRepository).insert({
workflowId: workflow.id,
versionId: activeVersionId,
nodes: workflow.nodes,
connections: workflow.connections,
authors: user?.email ?? '[email protected]',
});

await setActiveVersion(workflow.id, activeVersionId);

workflow.activeVersionId = activeVersionId;
return workflow;
}
40 changes: 38 additions & 2 deletions packages/@n8n/backend-test-utils/src/test-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,44 @@ type EntityName =
*/
export async function truncate(entities: EntityName[]) {
const connection = Container.get(Connection);
const dbType = connection.options.type;

for (const name of entities) {
await connection.getRepository(name).delete({});
// Disable FK checks for MySQL/MariaDB to handle circular dependencies
if (dbType === 'mysql' || dbType === 'mariadb') {
await connection.query('SET FOREIGN_KEY_CHECKS=0');
}

try {
// Collect junction tables to clean
const junctionTablesToClean = new Set<string>();

// Find all junction tables associated with the entities being truncated
for (const name of entities) {
try {
const metadata = connection.getMetadata(name);
for (const relation of metadata.manyToManyRelations) {
if (relation.junctionEntityMetadata) {
const junctionTableName = relation.junctionEntityMetadata.tablePath;
junctionTablesToClean.add(junctionTableName);
}
}
} catch (error) {
// Skip
}
}

// Clean junction tables first (since they reference the entities)
for (const tableName of junctionTablesToClean) {
await connection.query(`DELETE FROM ${tableName}`);
}

for (const name of entities) {
await connection.getRepository(name).delete({});
}
} finally {
// Re-enable FK checks
if (dbType === 'mysql' || dbType === 'mariadb') {
await connection.query('SET FOREIGN_KEY_CHECKS=1');
}
}
}
3 changes: 3 additions & 0 deletions packages/@n8n/db/src/entities/types-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { SharedWorkflow } from './shared-workflow';
import type { TagEntity } from './tag-entity';
import type { User } from './user';
import type { WorkflowEntity } from './workflow-entity';
import type { WorkflowHistory } from './workflow-history';

export type UsageCount = {
usageCount: number;
Expand Down Expand Up @@ -79,6 +80,7 @@ export interface IWorkflowDb extends IWorkflowBase {
triggerCount: number;
tags?: TagEntity[];
parentFolder?: Folder | null;
activeVersion?: WorkflowHistory | null;
}

export interface ICredentialsDb extends ICredentialsBase, ICredentialsEncrypted {
Expand Down Expand Up @@ -221,6 +223,7 @@ export namespace ListQueryDb {
| 'name'
| 'active'
| 'versionId'
| 'activeVersionId'
| 'createdAt'
| 'updatedAt'
| 'tags'
Expand Down
8 changes: 8 additions & 0 deletions packages/@n8n/db/src/entities/workflow-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { SharedWorkflow } from './shared-workflow';
import type { TagEntity } from './tag-entity';
import type { TestRun } from './test-run.ee';
import type { ISimplifiedPinData, IWorkflowDb } from './types-db';
import type { WorkflowHistory } from './workflow-history';
import type { WorkflowStatistics } from './workflow-statistics';
import type { WorkflowTagMapping } from './workflow-tag-mapping';
import { objectRetriever, sqlite } from '../utils/transformers';
Expand Down Expand Up @@ -103,6 +104,13 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl
@Column({ length: 36 })
versionId: string;

@Column({ name: 'activeVersionId', length: 36, nullable: true })
activeVersionId: string | null;

@ManyToOne('WorkflowHistory', { nullable: true })
@JoinColumn({ name: 'activeVersionId', referencedColumnName: 'versionId' })
activeVersion: WorkflowHistory | null;

@Column({ default: 1 })
versionCounter: number;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';

const WORKFLOWS_TABLE_NAME = 'workflow_entity';
const WORKFLOW_HISTORY_TABLE_NAME = 'workflow_history';

export class AddActiveVersionIdColumn1763047800000 implements ReversibleMigration {
async up({
schemaBuilder: { addColumns, column, addForeignKey },
queryRunner,
escape,
}: MigrationContext) {
const workflowsTableName = escape.tableName(WORKFLOWS_TABLE_NAME);

await addColumns(WORKFLOWS_TABLE_NAME, [column('activeVersionId').varchar(36)]);

await addForeignKey(
WORKFLOWS_TABLE_NAME,
'activeVersionId',
[WORKFLOW_HISTORY_TABLE_NAME, 'versionId'],
undefined,
'RESTRICT',
);

// For existing ACTIVE workflows, set activeVersionId = versionId
const versionIdColumn = escape.columnName('versionId');
const activeColumn = escape.columnName('active');
const activeVersionIdColumn = escape.columnName('activeVersionId');

await queryRunner.query(
`UPDATE ${workflowsTableName}
SET ${activeVersionIdColumn} = ${versionIdColumn}
WHERE ${activeColumn} = true`,
);
}

async down({ schemaBuilder: { dropColumns, dropForeignKey } }: MigrationContext) {
await dropForeignKey(WORKFLOWS_TABLE_NAME, 'activeVersionId', [
WORKFLOW_HISTORY_TABLE_NAME,
'versionId',
]);
await dropColumns(WORKFLOWS_TABLE_NAME, ['activeVersionId']);
}
}
6 changes: 4 additions & 2 deletions packages/@n8n/db/src/migrations/mysqldb/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { AddMfaColumns1690000000030 } from './../common/1690000000040-AddMfaColumns';
import { AddWorkflowHistoryAutoSaveFields1762847206508 } from './../common/1762847206508-AddWorkflowHistoryAutoSaveFields';
import { InitialMigration1588157391238 } from './1588157391238-InitialMigration';
import { WebhookModel1592447867632 } from './1592447867632-WebhookModel';
import { CreateIndexStoppedAt1594902918301 } from './1594902918301-CreateIndexStoppedAt';
Expand Down Expand Up @@ -55,6 +53,7 @@ import { AddToolsColumnToChatHubTables1761830340990 } from './1761830340990-AddT
import { CreateLdapEntities1674509946020 } from '../common/1674509946020-CreateLdapEntities';
import { PurgeInvalidWorkflowConnections1675940580449 } from '../common/1675940580449-PurgeInvalidWorkflowConnections';
import { RemoveResetPasswordColumns1690000000030 } from '../common/1690000000030-RemoveResetPasswordColumns';
import { AddMfaColumns1690000000030 } from '../common/1690000000040-AddMfaColumns';
import { CreateWorkflowNameIndex1691088862123 } from '../common/1691088862123-CreateWorkflowNameIndex';
import { CreateWorkflowHistoryTable1692967111175 } from '../common/1692967111175-CreateWorkflowHistoryTable';
import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-ExecutionSoftDelete';
Expand Down Expand Up @@ -114,6 +113,8 @@ import { CreateWorkflowDependencyTable1760314000000 } from '../common/1760314000
import { AddAttachmentsToChatHubMessages1761773155024 } from '../common/1761773155024-AddAttachmentsToChatHubMessages';
import { AddWorkflowDescriptionColumn1762177736257 } from '../common/1762177736257-AddWorkflowDescriptionColumn';
import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/1762763704614-BackfillMissingWorkflowHistoryRecords';
import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields';
import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn';
import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar';
import type { Migration } from '../migration-types';

Expand Down Expand Up @@ -235,4 +236,5 @@ export const mysqlMigrations: Migration[] = [
AddToolsColumnToChatHubTables1761830340990,
ChangeOAuthStateColumnToUnboundedVarchar1763572724000,
AddAttachmentsToChatHubMessages1761773155024,
AddActiveVersionIdColumn1763047800000,
];
2 changes: 2 additions & 0 deletions packages/@n8n/db/src/migrations/postgresdb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import { AddToolsColumnToChatHubTables1761830340990 } from '../common/1761830340
import { AddWorkflowDescriptionColumn1762177736257 } from '../common/1762177736257-AddWorkflowDescriptionColumn';
import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/1762763704614-BackfillMissingWorkflowHistoryRecords';
import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields';
import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn';
import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar';
import type { Migration } from '../migration-types';

Expand Down Expand Up @@ -235,4 +236,5 @@ export const postgresMigrations: Migration[] = [
AddToolsColumnToChatHubTables1761830340990,
ChangeOAuthStateColumnToUnboundedVarchar1763572724000,
AddAttachmentsToChatHubMessages1761773155024,
AddActiveVersionIdColumn1763047800000,
];
2 changes: 2 additions & 0 deletions packages/@n8n/db/src/migrations/sqlite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import { AddToolsColumnToChatHubTables1761830340990 } from '../common/1761830340
import { AddWorkflowDescriptionColumn1762177736257 } from '../common/1762177736257-AddWorkflowDescriptionColumn';
import { BackfillMissingWorkflowHistoryRecords1762763704614 } from '../common/1762763704614-BackfillMissingWorkflowHistoryRecords';
import { AddWorkflowHistoryAutoSaveFields1762847206508 } from '../common/1762847206508-AddWorkflowHistoryAutoSaveFields';
import { AddActiveVersionIdColumn1763047800000 } from '../common/1763047800000-AddActiveVersionIdColumn';
import { ChangeOAuthStateColumnToUnboundedVarchar1763572724000 } from '../common/1763572724000-ChangeOAuthStateColumnToUnboundedVarchar';
import type { Migration } from '../migration-types';

Expand Down Expand Up @@ -227,6 +228,7 @@ const sqliteMigrations: Migration[] = [
AddToolsColumnToChatHubTables1761830340990,
ChangeOAuthStateColumnToUnboundedVarchar1763572724000,
AddAttachmentsToChatHubMessages1761773155024,
AddActiveVersionIdColumn1763047800000,
];

export { sqliteMigrations };
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,7 @@ describe('WorkflowRepository', () => {
}),
);

expect(queryBuilder.andWhere).toHaveBeenCalledWith('workflow.active = :active', {
active: true,
});
expect(queryBuilder.andWhere).toHaveBeenCalledWith('workflow.activeVersionId IS NOT NULL');

expect(queryBuilder.innerJoin).toHaveBeenCalledWith('workflow.shared', 'shared');
expect(queryBuilder.andWhere).toHaveBeenCalledWith('shared.projectId = :projectId', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class LicenseMetricsRepository extends Repository<LicenseMetrics> {
SELECT
(SELECT COUNT(*) FROM ${userTable} WHERE disabled = false) AS enabled_user_count,
(SELECT COUNT(*) FROM ${userTable}) AS total_user_count,
(SELECT COUNT(*) FROM ${workflowTable} WHERE active = true) AS active_workflow_count,
(SELECT COUNT(*) FROM ${workflowTable} WHERE ${this.toColumnName('activeVersionId')} IS NOT NULL) AS active_workflow_count,
(SELECT COUNT(*) FROM ${workflowTable}) AS total_workflow_count,
(SELECT COUNT(*) FROM ${credentialTable}) AS total_credentials_count,
(SELECT SUM(count) FROM ${workflowStatsTable} WHERE name IN ('production_success', 'production_error')) AS production_executions_count,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,15 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
where?: FindOptionsWhere<SharedWorkflow>;
includeTags?: boolean;
includeParentFolder?: boolean;
includeActiveVersion?: boolean;
em?: EntityManager;
} = {},
) {
const {
where = {},
includeTags = false,
includeParentFolder = false,
includeActiveVersion = false,
em = this.manager,
} = options;

Expand All @@ -169,6 +171,7 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
shared: { project: { projectRelations: { user: true } } },
tags: includeTags,
parentFolder: includeParentFolder,
activeVersion: includeActiveVersion,
},
},
});
Expand Down
Loading
Loading