Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ packages/ui/src/icons

# Database
data/

#IDE
.idea
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { UncompleteTask } from '@/commands/uncomplete-task.domain-command';
import { AbstractApplicationCommand } from '@/module/application-command-events';

export class UncompleteTaskApplicationCommand extends AbstractApplicationCommand<UncompleteTask> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UncompleteTask = {
type: 'UncompleteTask';
data: { learningMaterialsId: string; taskId: string };
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

import { CompleteTaskApplicationCommand } from '@/commands/complete-task.application-command';
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-event';
import { APPLICATION_SERVICE, ApplicationService } from '@/write/shared/application/application-service';
import { EventStreamName } from '@/write/shared/application/event-stream-name.value-object';
Expand All @@ -18,7 +19,7 @@ export class CompleteTaskCommandHandler implements ICommandHandler<CompleteTaskA
async execute(command: CompleteTaskApplicationCommand): Promise<void> {
const eventStream = EventStreamName.from('LearningMaterialsTasks', command.data.learningMaterialsId);

await this.applicationService.execute<TaskWasCompleted>(
await this.applicationService.execute<TaskWasCompleted | TaskWasUncompleted>(
eventStream,
{
causationId: command.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
import { UncompleteTaskApplicationCommand } from '@/module/commands/uncomplete-task.application-command';
import { uncompleteTask } from '@/write/learning-materials-tasks/domain/uncomplete-task';
import { APPLICATION_SERVICE, ApplicationService } from '@/write/shared/application/application-service';
import { EventStreamName } from '@/write/shared/application/event-stream-name.value-object';

@CommandHandler(UncompleteTaskApplicationCommand)
export class UncompleteTaskCommandHandler implements ICommandHandler<UncompleteTaskApplicationCommand> {
constructor(
@Inject(APPLICATION_SERVICE)
private readonly applicationService: ApplicationService,
) {}

async execute(command: UncompleteTaskApplicationCommand): Promise<void> {
const eventStream = EventStreamName.from('LearningMaterialsTasks', command.data.learningMaterialsId);

await this.applicationService.execute<TaskWasCompleted | TaskWasUncompleted>(
eventStream,
{
causationId: command.id,
correlationId: command.metadata.correlationId,
},
(pastEvents) => uncompleteTask(pastEvents, command),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
import { CompleteTask } from '@/module/commands/complete-task.domain-command';
import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-event';

Expand Down Expand Up @@ -61,4 +62,50 @@ describe('complete task', () => {
// Then
expect(events).toThrowError('Task was already completed');
});

it('should complete uncompleted task', () => {
// given
const pastEvents: TaskWasUncompleted[] = [
{
type: 'TaskWasUncompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
},
];

// when
const events = completeTask(pastEvents, command);

// then
expect(events).toStrictEqual([
{
type: 'TaskWasCompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
},
]);
});

it('should complete task if task was completed and then uncompleted', () => {
// given
const pastEvents: (TaskWasCompleted | TaskWasUncompleted)[] = [
{
type: 'TaskWasCompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
},
{
type: 'TaskWasUncompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
},
];

// when
const events = completeTask(pastEvents, command);

// then
expect(events).toStrictEqual([
{
type: 'TaskWasCompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
import { CompleteTask } from '@/module/commands/complete-task.domain-command';

export function completeTask(
pastEvents: TaskWasCompleted[],
pastEvents: (TaskWasCompleted | TaskWasUncompleted)[],
{ data: { learningMaterialsId, taskId } }: CompleteTask,
): TaskWasCompleted[] {
const state = pastEvents
Expand All @@ -13,6 +14,9 @@ export function completeTask(
case 'TaskWasCompleted': {
return { completed: true };
}
case 'TaskWasUncompleted': {
return { completed: false };
}
default: {
return acc;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { UncompleteTask } from '@/commands/uncomplete-task.domain-command';
import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
import { uncompleteTask } from '@/write/learning-materials-tasks/domain/uncomplete-task';

describe('uncomplete task', () => {
const command: UncompleteTask = {
type: 'UncompleteTask',
data: { learningMaterialsId: 'sbAPITNMsl2wW6j2cg1H2A', taskId: 'L9EXtwmBNBXgo_qh0uzbq' },
};

it('should uncomplete completed task', () => {
// Given
const pastEvents: TaskWasCompleted[] = [
{
type: 'TaskWasCompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: 'L9EXtwmBNBXgo_qh0uzbq' },
},
];

// When
const events = uncompleteTask(pastEvents, command);

// Then
expect(events).toStrictEqual([
{
type: 'TaskWasUncompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
},
]);
});

it('should throw an error if try to uncomplete uncompleted task', () => {
// given
const pastEvents: TaskWasUncompleted[] = [
{
type: 'TaskWasUncompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
},
];

// when
const events = () => uncompleteTask(pastEvents, command);

// then
expect(events).toThrowError('Can not uncomplete task that was not completed yet.');
});

it('should throw an error if try to uncomplete task that was neither completed nor uncompleted yet', () => {
// given
const pastEvents: (TaskWasCompleted | TaskWasUncompleted)[] = [];

// when
const events = () => uncompleteTask(pastEvents, command);

// then
expect(events).toThrowError('Can not uncomplete task that was not completed yet.');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { UncompleteTask } from '@/commands/uncomplete-task.domain-command';
import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';

export function uncompleteTask(
pastEvents: (TaskWasCompleted | TaskWasUncompleted)[],
{ data: { learningMaterialsId, taskId } }: UncompleteTask,
): TaskWasUncompleted[] {
const state = pastEvents
.filter(({ data }) => data.taskId === taskId)
.reduce<{ completed: boolean }>(
(acc, event) => {
switch (event.type) {
case 'TaskWasCompleted': {
return { completed: true };
}
case 'TaskWasUncompleted': {
return { completed: false };
}
default: {
return acc;
}
}
},
{ completed: false },
);

if (!state.completed) {
throw new Error('Can not uncomplete task that was not completed yet.');
}

const newEvent: TaskWasUncompleted = {
type: 'TaskWasUncompleted',
data: {
taskId,
learningMaterialsId,
},
};

return [newEvent];
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
import { AsyncReturnType } from 'type-fest';

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

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

enum CommandType {
COMPLETE_TASK = 'Complete Task',
UNCOMPLETE_TASK = 'Uncomplete Task',
}

describe('learning materials tasks', () => {
let module: AsyncReturnType<typeof learningMaterialsTasksTestModule>;
const commandBuilder = (taskId = 'VmkxXnPG02CaUNV8Relzk', learningMaterialsId = 'ZpMpw2eh1llFCGKZJEN6r') => ({
class: CompleteTaskApplicationCommand,
type: 'CompleteTask',
const commandBuilder = (
type: string,
taskId = 'VmkxXnPG02CaUNV8Relzk',
learningMaterialsId = 'ZpMpw2eh1llFCGKZJEN6r',
) => ({
class: type === CommandType.COMPLETE_TASK ? CompleteTaskApplicationCommand : UncompleteTaskApplicationCommand,
type,
data: { taskId, learningMaterialsId },
});

beforeEach(async () => {
module = await learningMaterialsTasksTestModule();
});

afterEach(async () => {
await module.close();
});

it('should change state of the task to complete', async () => {
// Given
const command = commandBuilder();
const command = commandBuilder(CommandType.COMPLETE_TASK);

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

it('should not change task state if task is already completed', async () => {
// Given
const command = commandBuilder();
const command = commandBuilder(CommandType.COMPLETE_TASK);

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

beforeEach(async () => {
module = await learningMaterialsTasksTestModule();
it('should change state of the task to uncomplete when task was completed already', async () => {
// Given
const completeCommand = commandBuilder(CommandType.COMPLETE_TASK);
const uncompleteCommand = commandBuilder(CommandType.UNCOMPLETE_TASK);

await module.executeCommand(() => completeCommand);

// When
await module.executeCommand(() => uncompleteCommand);

// Then
module.expectEventPublishedLastly<TaskWasUncompleted>({
type: 'TaskWasUncompleted',
data: {
learningMaterialsId: uncompleteCommand.data.learningMaterialsId,
taskId: uncompleteCommand.data.taskId,
},
streamName: EventStreamName.from('LearningMaterialsTasks', uncompleteCommand.data.learningMaterialsId),
});
});

afterEach(async () => {
await module.close();
it('should not change state of the task to uncomplete if task was not completed before', async () => {
// Given
const uncompleteCommand = commandBuilder(CommandType.UNCOMPLETE_TASK);

// When&Then
await expect(() => module.executeCommand(() => uncompleteCommand)).rejects.toThrow(
'Can not uncomplete task that was not completed yet.',
);
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Module } from '@nestjs/common';

import { UncompleteTaskCommandHandler } from '@/write/learning-materials-tasks/application/uncomplete-task.command-handler';

import { SharedModule } from '../shared/shared.module';
import { CompleteTaskCommandHandler } from './application/complete-task.command-handler';
import { LearningMaterialsTaskRestController } from './presentation/rest/process-st-events.rest-controller';

@Module({
imports: [SharedModule],
providers: [CompleteTaskCommandHandler],
providers: [CompleteTaskCommandHandler, UncompleteTaskCommandHandler],
controllers: [LearningMaterialsTaskRestController],
})
export class LearningMaterialsTasksModule {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';

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

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

await this.commandBus.execute(command);
}

const command = this.commandFactory.applicationCommand(() => ({
class: UncompleteTaskApplicationCommand,
type: 'UncompleteTask',
data: { learningMaterialsId, taskId },
}));

await this.commandBus.execute(command);
}
}