Set statuses on workflows (#6792)

Add listener to keep status on workflows up to date:
- version draft => statuses should contain draft
- version active => statuses should contain active
- version deactivated => if no version active, statuses should contain
deactivated

Renaming also the endpoints because it was not reflecting the full
behaviour.

Finally, adding a new status Archived for versions. Will be used when a
version is deactivated, but is not the last published version anymore.
It means this version cannot be re-activated.
This commit is contained in:
Thomas Trompette
2024-08-30 18:06:04 +02:00
committed by GitHub
parent f7c99ddc7a
commit a3ea0acd1a
15 changed files with 1010 additions and 92 deletions
@@ -0,0 +1,268 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import {
WorkflowStatusesUpdateJob,
WorkflowVersionBatchEvent,
WorkflowVersionEventType,
} from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
describe('WorkflowStatusesUpdate', () => {
let job: WorkflowStatusesUpdateJob;
const mockWorkflowRepository = {
findOneOrFail: jest.fn(),
update: jest.fn(),
};
const mockTwentyORMManager = {
getRepository: jest.fn().mockResolvedValue(mockWorkflowRepository),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkflowStatusesUpdateJob,
{
provide: TwentyORMManager,
useValue: mockTwentyORMManager,
},
],
}).compile();
job = await module.resolve<WorkflowStatusesUpdateJob>(
WorkflowStatusesUpdateJob,
);
});
it('should be defined', () => {
expect(job).toBeDefined();
});
describe('handle', () => {
describe('when event type is CREATE', () => {
it('when already a draft, do not change anything', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.CREATE,
workflowIds: ['1'],
};
const mockWorkflow = {
statuses: [WorkflowStatus.DRAFT],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledTimes(0);
});
it('when no draft yet, update statuses', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.CREATE,
workflowIds: ['1'],
};
const mockWorkflow = {
id: '1',
statuses: [WorkflowStatus.ACTIVE],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledWith(
{ id: '1' },
{ statuses: [WorkflowStatus.ACTIVE, WorkflowStatus.DRAFT] },
);
});
});
describe('when event type is STATUS_UPDATE', () => {
test('when status is the same, should not do anything', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.STATUS_UPDATE,
statusUpdates: [
{
workflowId: '1',
previousStatus: WorkflowVersionStatus.ACTIVE,
newStatus: WorkflowVersionStatus.ACTIVE,
},
],
};
const mockWorkflow = {
statuses: [WorkflowStatus.ACTIVE],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledTimes(0);
});
test('when update that should be impossible, do not do anything', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.STATUS_UPDATE,
statusUpdates: [
{
workflowId: '1',
previousStatus: WorkflowVersionStatus.ACTIVE,
newStatus: WorkflowVersionStatus.DRAFT,
},
],
};
const mockWorkflow = {
statuses: [WorkflowStatus.ACTIVE],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledTimes(0);
});
test('when WorkflowVersionStatus.DEACTIVATED to WorkflowVersionStatus.ACTIVE, should activate', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.STATUS_UPDATE,
statusUpdates: [
{
workflowId: '1',
previousStatus: WorkflowVersionStatus.DEACTIVATED,
newStatus: WorkflowVersionStatus.ACTIVE,
},
],
};
const mockWorkflow = {
statuses: [WorkflowStatus.DEACTIVATED],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledWith(
{ id: '1' },
{ statuses: [WorkflowStatus.ACTIVE] },
);
});
test('when WorkflowVersionStatus.ACTIVE to WorkflowVersionStatus.DEACTIVATED, should deactivate', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.STATUS_UPDATE,
statusUpdates: [
{
workflowId: '1',
previousStatus: WorkflowVersionStatus.ACTIVE,
newStatus: WorkflowVersionStatus.DEACTIVATED,
},
],
};
const mockWorkflow = {
statuses: [WorkflowStatus.ACTIVE],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledWith(
{ id: '1' },
{ statuses: [WorkflowStatus.DEACTIVATED] },
);
});
test('when WorkflowVersionStatus.DRAFT to WorkflowVersionStatus.ACTIVE, should activate', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.STATUS_UPDATE,
statusUpdates: [
{
workflowId: '1',
previousStatus: WorkflowVersionStatus.DRAFT,
newStatus: WorkflowVersionStatus.ACTIVE,
},
],
};
const mockWorkflow = {
statuses: [WorkflowStatus.DRAFT],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledWith(
{ id: '1' },
{ statuses: [WorkflowStatus.ACTIVE] },
);
});
});
describe('when event type is DELETE', () => {
test('when status is not draft, should not do anything', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.DELETE,
workflowIds: ['1'],
};
const mockWorkflow = {
statuses: [WorkflowStatus.ACTIVE],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledTimes(0);
});
test('when status is draft, should delete', async () => {
const event: WorkflowVersionBatchEvent = {
workspaceId: '1',
type: WorkflowVersionEventType.DELETE,
workflowIds: ['1'],
};
const mockWorkflow = {
statuses: [WorkflowStatus.DRAFT],
};
mockWorkflowRepository.findOneOrFail.mockResolvedValue(mockWorkflow);
await job.handle(event);
expect(mockWorkflowRepository.findOneOrFail).toHaveBeenCalledTimes(1);
expect(mockWorkflowRepository.update).toHaveBeenCalledWith(
{ id: '1' },
{ statuses: [] },
);
});
});
});
});
@@ -0,0 +1,201 @@
import { Scope } from '@nestjs/common';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { getStatusCombinationFromArray } from 'src/modules/workflow/workflow-status/utils/get-status-combination-from-array.util';
import { getStatusCombinationFromUpdate } from 'src/modules/workflow/workflow-status/utils/get-status-combination-from-update.util';
import { getWorkflowStatusesFromCombination } from 'src/modules/workflow/workflow-status/utils/get-statuses-from-combination.util';
export enum WorkflowVersionEventType {
CREATE = 'CREATE',
STATUS_UPDATE = 'STATUS_UPDATE',
DELETE = 'DELETE',
}
export type WorkflowVersionBatchEvent = {
workspaceId: string;
} & (
| WorkflowVersionBatchCreateEvent
| WorkflowVersionBatchStatusUpdate
| WorkflowVersionBatchDelete
);
export type WorkflowVersionBatchCreateEvent = {
type: WorkflowVersionEventType.CREATE;
} & {
workflowIds: string[];
};
export type WorkflowVersionStatusUpdate = {
workflowId: string;
previousStatus: WorkflowVersionStatus;
newStatus: WorkflowVersionStatus;
};
export type WorkflowVersionBatchStatusUpdate = {
type: WorkflowVersionEventType.STATUS_UPDATE;
} & {
statusUpdates: WorkflowVersionStatusUpdate[];
};
export type WorkflowVersionBatchDelete = {
type: WorkflowVersionEventType.DELETE;
} & { workflowIds: string[] };
@Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST })
export class WorkflowStatusesUpdateJob {
constructor(private readonly twentyORMManager: TwentyORMManager) {}
@Process(WorkflowStatusesUpdateJob.name)
async handle(event: WorkflowVersionBatchEvent): Promise<void> {
switch (event.type) {
case WorkflowVersionEventType.CREATE:
await Promise.all(
event.workflowIds.map((workflowId) =>
this.handleWorkflowVersionCreated(workflowId),
),
);
break;
case WorkflowVersionEventType.STATUS_UPDATE:
await Promise.all(
event.statusUpdates.map((statusUpdate) =>
this.handleWorkflowVersionStatusUpdated(statusUpdate),
),
);
break;
case WorkflowVersionEventType.DELETE:
await Promise.all(
event.workflowIds.map((workflowId) =>
this.handleWorkflowVersionDeleted(workflowId),
),
);
break;
default:
break;
}
}
private async handleWorkflowVersionCreated(
workflowId: string,
): Promise<void> {
const workflowRepository =
await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>(
'workflow',
);
const workflow = await workflowRepository.findOneOrFail({
where: {
id: workflowId,
},
});
const currentWorkflowStatusCombination = getStatusCombinationFromArray(
workflow.statuses || [],
);
const newWorkflowStatusCombination = getStatusCombinationFromUpdate(
currentWorkflowStatusCombination,
undefined,
WorkflowVersionStatus.DRAFT,
);
if (newWorkflowStatusCombination === currentWorkflowStatusCombination) {
return;
}
await workflowRepository.update(
{
id: workflow.id,
},
{
statuses: getWorkflowStatusesFromCombination(
newWorkflowStatusCombination,
),
},
);
}
private async handleWorkflowVersionStatusUpdated(
statusUpdate: WorkflowVersionStatusUpdate,
): Promise<void> {
const workflowRepository =
await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>(
'workflow',
);
const workflow = await workflowRepository.findOneOrFail({
where: {
id: statusUpdate.workflowId,
},
});
const currentWorkflowStatusCombination = getStatusCombinationFromArray(
workflow.statuses || [],
);
const newWorkflowStatusCombination = getStatusCombinationFromUpdate(
currentWorkflowStatusCombination,
statusUpdate.previousStatus,
statusUpdate.newStatus,
);
if (newWorkflowStatusCombination === currentWorkflowStatusCombination) {
return;
}
await workflowRepository.update(
{
id: statusUpdate.workflowId,
},
{
statuses: getWorkflowStatusesFromCombination(
newWorkflowStatusCombination,
),
},
);
}
private async handleWorkflowVersionDeleted(
workflowId: string,
): Promise<void> {
const workflowRepository =
await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>(
'workflow',
);
const workflow = await workflowRepository.findOneOrFail({
where: {
id: workflowId,
},
});
const currentWorkflowStatusCombination = getStatusCombinationFromArray(
workflow.statuses || [],
);
const newWorkflowStatusCombination = getStatusCombinationFromUpdate(
currentWorkflowStatusCombination,
WorkflowVersionStatus.DRAFT,
undefined,
);
if (newWorkflowStatusCombination === currentWorkflowStatusCombination) {
return;
}
await workflowRepository.update(
{
id: workflowId,
},
{
statuses: getWorkflowStatusesFromCombination(
newWorkflowStatusCombination,
),
},
);
}
}