Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Commit 9e1015a

Browse files
feat(api): create slice UncompleteTask -> TaskWasUncompleted (#230) (#395)
1 parent cad51b7 commit 9e1015a

13 files changed

+263
-13
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ packages/ui/src/icons
5959

6060
# Database
6161
data/
62+
63+
#IDE
64+
.idea
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { UncompleteTask } from '@/commands/uncomplete-task.domain-command';
2+
import { AbstractApplicationCommand } from '@/module/application-command-events';
3+
4+
export class UncompleteTaskApplicationCommand extends AbstractApplicationCommand<UncompleteTask> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type UncompleteTask = {
2+
type: 'UncompleteTask';
3+
data: { learningMaterialsId: string; taskId: string };
4+
};

packages/api/src/module/write/learning-materials-tasks/application/complete-task.command-handler.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Inject } from '@nestjs/common';
22
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
33

44
import { CompleteTaskApplicationCommand } from '@/commands/complete-task.application-command';
5-
import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-event';
5+
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
66
import { APPLICATION_SERVICE, ApplicationService } from '@/write/shared/application/application-service';
77
import { EventStreamName } from '@/write/shared/application/event-stream-name.value-object';
88

@@ -18,7 +18,7 @@ export class CompleteTaskCommandHandler implements ICommandHandler<CompleteTaskA
1818
async execute(command: CompleteTaskApplicationCommand): Promise<void> {
1919
const eventStream = EventStreamName.from('LearningMaterialsTasks', command.data.learningMaterialsId);
2020

21-
await this.applicationService.execute<TaskWasCompleted>(
21+
await this.applicationService.execute<LearningMaterialsTasksDomainEvent>(
2222
eventStream,
2323
{
2424
causationId: command.id,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Inject } from '@nestjs/common';
2+
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
3+
4+
import { UncompleteTaskApplicationCommand } from '@/module/commands/uncomplete-task.application-command';
5+
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
6+
import { uncompleteTask } from '@/write/learning-materials-tasks/domain/uncomplete-task';
7+
import { APPLICATION_SERVICE, ApplicationService } from '@/write/shared/application/application-service';
8+
import { EventStreamName } from '@/write/shared/application/event-stream-name.value-object';
9+
10+
@CommandHandler(UncompleteTaskApplicationCommand)
11+
export class UncompleteTaskCommandHandler implements ICommandHandler<UncompleteTaskApplicationCommand> {
12+
constructor(
13+
@Inject(APPLICATION_SERVICE)
14+
private readonly applicationService: ApplicationService,
15+
) {}
16+
17+
async execute(command: UncompleteTaskApplicationCommand): Promise<void> {
18+
const eventStream = EventStreamName.from('LearningMaterialsTasks', command.data.learningMaterialsId);
19+
20+
await this.applicationService.execute<LearningMaterialsTasksDomainEvent>(
21+
eventStream,
22+
{
23+
causationId: command.id,
24+
correlationId: command.metadata.correlationId,
25+
},
26+
(pastEvents) => uncompleteTask(pastEvents, command),
27+
);
28+
}
29+
}

packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts

+48
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
12
import { CompleteTask } from '@/module/commands/complete-task.domain-command';
23
import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-event';
4+
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
35

46
import { completeTask } from './complete-task';
57

@@ -61,4 +63,50 @@ describe('complete task', () => {
6163
// Then
6264
expect(events).toThrowError('Task was already completed');
6365
});
66+
67+
it('should complete uncompleted task', () => {
68+
// given
69+
const pastEvents: TaskWasUncompleted[] = [
70+
{
71+
type: 'TaskWasUncompleted',
72+
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
73+
},
74+
];
75+
76+
// when
77+
const events = completeTask(pastEvents, command);
78+
79+
// then
80+
expect(events).toStrictEqual([
81+
{
82+
type: 'TaskWasCompleted',
83+
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
84+
},
85+
]);
86+
});
87+
88+
it('should complete task if task was completed and then uncompleted', () => {
89+
// given
90+
const pastEvents: LearningMaterialsTasksDomainEvent[] = [
91+
{
92+
type: 'TaskWasCompleted',
93+
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
94+
},
95+
{
96+
type: 'TaskWasUncompleted',
97+
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
98+
},
99+
];
100+
101+
// when
102+
const events = completeTask(pastEvents, command);
103+
104+
// then
105+
expect(events).toStrictEqual([
106+
{
107+
type: 'TaskWasCompleted',
108+
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
109+
},
110+
]);
111+
});
64112
});

packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
22
import { CompleteTask } from '@/module/commands/complete-task.domain-command';
3+
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
34

45
export function completeTask(
5-
pastEvents: TaskWasCompleted[],
6+
pastEvents: LearningMaterialsTasksDomainEvent[],
67
{ data: { learningMaterialsId, taskId } }: CompleteTask,
78
): TaskWasCompleted[] {
89
const state = pastEvents
@@ -13,6 +14,9 @@ export function completeTask(
1314
case 'TaskWasCompleted': {
1415
return { completed: true };
1516
}
17+
case 'TaskWasUncompleted': {
18+
return { completed: false };
19+
}
1620
default: {
1721
return acc;
1822
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
2+
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
3+
4+
export type LearningMaterialsTasksDomainEvent = TaskWasCompleted | TaskWasUncompleted;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { UncompleteTask } from '@/commands/uncomplete-task.domain-command';
2+
import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
3+
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
4+
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
5+
import { uncompleteTask } from '@/write/learning-materials-tasks/domain/uncomplete-task';
6+
7+
describe('uncomplete task', () => {
8+
const command: UncompleteTask = {
9+
type: 'UncompleteTask',
10+
data: { learningMaterialsId: 'sbAPITNMsl2wW6j2cg1H2A', taskId: 'L9EXtwmBNBXgo_qh0uzbq' },
11+
};
12+
13+
it('should uncomplete completed task', () => {
14+
// Given
15+
const pastEvents: TaskWasCompleted[] = [
16+
{
17+
type: 'TaskWasCompleted',
18+
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: 'L9EXtwmBNBXgo_qh0uzbq' },
19+
},
20+
];
21+
22+
// When
23+
const events = uncompleteTask(pastEvents, command);
24+
25+
// Then
26+
expect(events).toStrictEqual([
27+
{
28+
type: 'TaskWasUncompleted',
29+
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
30+
},
31+
]);
32+
});
33+
34+
it('should throw an error if try to uncomplete uncompleted task', () => {
35+
// given
36+
const pastEvents: TaskWasUncompleted[] = [
37+
{
38+
type: 'TaskWasUncompleted',
39+
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
40+
},
41+
];
42+
43+
// when
44+
const events = () => uncompleteTask(pastEvents, command);
45+
46+
// then
47+
expect(events).toThrowError('Can not uncomplete task that was not completed yet.');
48+
});
49+
50+
it('should throw an error if try to uncomplete task that was neither completed nor uncompleted yet', () => {
51+
// given
52+
const pastEvents: LearningMaterialsTasksDomainEvent[] = [];
53+
54+
// when
55+
const events = () => uncompleteTask(pastEvents, command);
56+
57+
// then
58+
expect(events).toThrowError('Can not uncomplete task that was not completed yet.');
59+
});
60+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { UncompleteTask } from '@/commands/uncomplete-task.domain-command';
2+
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
3+
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
4+
5+
export function uncompleteTask(
6+
pastEvents: LearningMaterialsTasksDomainEvent[],
7+
{ data: { learningMaterialsId, taskId } }: UncompleteTask,
8+
): TaskWasUncompleted[] {
9+
const state = pastEvents
10+
.filter(({ data }) => data.taskId === taskId)
11+
.reduce<{ completed: boolean }>(
12+
(acc, event) => {
13+
switch (event.type) {
14+
case 'TaskWasCompleted': {
15+
return { completed: true };
16+
}
17+
case 'TaskWasUncompleted': {
18+
return { completed: false };
19+
}
20+
default: {
21+
return acc;
22+
}
23+
}
24+
},
25+
{ completed: false },
26+
);
27+
28+
if (!state.completed) {
29+
throw new Error('Can not uncomplete task that was not completed yet.');
30+
}
31+
32+
const newEvent: TaskWasUncompleted = {
33+
type: 'TaskWasUncompleted',
34+
data: {
35+
taskId,
36+
learningMaterialsId,
37+
},
38+
};
39+
40+
return [newEvent];
41+
}

packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts

+51-9
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
import { AsyncReturnType } from 'type-fest';
22

3+
import { UncompleteTaskApplicationCommand } from '@/commands/uncomplete-task.application-command';
4+
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
35
import { CompleteTaskApplicationCommand } from '@/module/commands/complete-task.application-command';
46
import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-event';
57

68
import { EventStreamName } from '../shared/application/event-stream-name.value-object';
79
import { learningMaterialsTasksTestModule } from './learning-materials-tasks.test-module';
810

11+
enum CommandType {
12+
COMPLETE_TASK = 'Complete Task',
13+
UNCOMPLETE_TASK = 'Uncomplete Task',
14+
}
15+
916
describe('learning materials tasks', () => {
1017
let module: AsyncReturnType<typeof learningMaterialsTasksTestModule>;
11-
const commandBuilder = (taskId = 'VmkxXnPG02CaUNV8Relzk', learningMaterialsId = 'ZpMpw2eh1llFCGKZJEN6r') => ({
12-
class: CompleteTaskApplicationCommand,
13-
type: 'CompleteTask',
18+
const commandBuilder = (
19+
type: string,
20+
taskId = 'VmkxXnPG02CaUNV8Relzk',
21+
learningMaterialsId = 'ZpMpw2eh1llFCGKZJEN6r',
22+
) => ({
23+
class: type === CommandType.COMPLETE_TASK ? CompleteTaskApplicationCommand : UncompleteTaskApplicationCommand,
24+
type,
1425
data: { taskId, learningMaterialsId },
1526
});
1627

28+
beforeEach(async () => {
29+
module = await learningMaterialsTasksTestModule();
30+
});
31+
32+
afterEach(async () => {
33+
await module.close();
34+
});
35+
1736
it('should change state of the task to complete', async () => {
1837
// Given
19-
const command = commandBuilder();
38+
const command = commandBuilder(CommandType.COMPLETE_TASK);
2039

2140
// When
2241
await module.executeCommand(() => command);
@@ -34,7 +53,7 @@ describe('learning materials tasks', () => {
3453

3554
it('should not change task state if task is already completed', async () => {
3655
// Given
37-
const command = commandBuilder();
56+
const command = commandBuilder(CommandType.COMPLETE_TASK);
3857

3958
// When
4059
await module.executeCommand(() => command);
@@ -43,11 +62,34 @@ describe('learning materials tasks', () => {
4362
await expect(() => module.executeCommand(() => command)).rejects.toThrow();
4463
});
4564

46-
beforeEach(async () => {
47-
module = await learningMaterialsTasksTestModule();
65+
it('should change state of the task to uncomplete when task was completed already', async () => {
66+
// Given
67+
const completeCommand = commandBuilder(CommandType.COMPLETE_TASK);
68+
const uncompleteCommand = commandBuilder(CommandType.UNCOMPLETE_TASK);
69+
70+
await module.executeCommand(() => completeCommand);
71+
72+
// When
73+
await module.executeCommand(() => uncompleteCommand);
74+
75+
// Then
76+
module.expectEventPublishedLastly<TaskWasUncompleted>({
77+
type: 'TaskWasUncompleted',
78+
data: {
79+
learningMaterialsId: uncompleteCommand.data.learningMaterialsId,
80+
taskId: uncompleteCommand.data.taskId,
81+
},
82+
streamName: EventStreamName.from('LearningMaterialsTasks', uncompleteCommand.data.learningMaterialsId),
83+
});
4884
});
4985

50-
afterEach(async () => {
51-
await module.close();
86+
it('should not change state of the task to uncomplete if task was not completed before', async () => {
87+
// Given
88+
const uncompleteCommand = commandBuilder(CommandType.UNCOMPLETE_TASK);
89+
90+
// When&Then
91+
await expect(() => module.executeCommand(() => uncompleteCommand)).rejects.toThrow(
92+
'Can not uncomplete task that was not completed yet.',
93+
);
5294
});
5395
});
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Module } from '@nestjs/common';
22

3+
import { UncompleteTaskCommandHandler } from '@/write/learning-materials-tasks/application/uncomplete-task.command-handler';
4+
35
import { SharedModule } from '../shared/shared.module';
46
import { CompleteTaskCommandHandler } from './application/complete-task.command-handler';
57
import { LearningMaterialsTaskRestController } from './presentation/rest/process-st-events.rest-controller';
68

79
@Module({
810
imports: [SharedModule],
9-
providers: [CompleteTaskCommandHandler],
11+
providers: [CompleteTaskCommandHandler, UncompleteTaskCommandHandler],
1012
controllers: [LearningMaterialsTaskRestController],
1113
})
1214
export class LearningMaterialsTasksModule {}

packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
22
import { CommandBus } from '@nestjs/cqrs';
33

4+
import { UncompleteTaskApplicationCommand } from '@/commands/uncomplete-task.application-command';
45
import { CompleteTaskApplicationCommand } from '@/module/commands/complete-task.application-command';
56
import { ApplicationCommandFactory } from '@/write/shared/application/application-command.factory';
67

@@ -24,5 +25,13 @@ export class LearningMaterialsTaskRestController {
2425

2526
await this.commandBus.execute(command);
2627
}
28+
29+
const command = this.commandFactory.applicationCommand(() => ({
30+
class: UncompleteTaskApplicationCommand,
31+
type: 'UncompleteTask',
32+
data: { learningMaterialsId, taskId },
33+
}));
34+
35+
await this.commandBus.execute(command);
2736
}
2837
}

0 commit comments

Comments
 (0)