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 all 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,7 +2,7 @@ import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

import { CompleteTaskApplicationCommand } from '@/commands/complete-task.application-command';
import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-event';
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
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 +18,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<LearningMaterialsTasksDomainEvent>(
eventStream,
{
causationId: command.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

import { UncompleteTaskApplicationCommand } from '@/module/commands/uncomplete-task.application-command';
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
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<LearningMaterialsTasksDomainEvent>(
eventStream,
{
causationId: command.id,
correlationId: command.metadata.correlationId,
},
(pastEvents) => uncompleteTask(pastEvents, command),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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';
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';

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

Expand Down Expand Up @@ -61,4 +63,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: LearningMaterialsTasksDomainEvent[] = [
{
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 { CompleteTask } from '@/module/commands/complete-task.domain-command';
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';

export function completeTask(
pastEvents: TaskWasCompleted[],
pastEvents: LearningMaterialsTasksDomainEvent[],
{ 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,4 @@
import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
import { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';

export type LearningMaterialsTasksDomainEvent = TaskWasCompleted | TaskWasUncompleted;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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 { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';
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: LearningMaterialsTasksDomainEvent[] = [];

// 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 { TaskWasUncompleted } from '@/events/task-was-uncompleted-event.domain-event';
import { LearningMaterialsTasksDomainEvent } from '@/write/learning-materials-tasks/domain/events';

export function uncompleteTask(
pastEvents: LearningMaterialsTasksDomainEvent[],
{ 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);
}
}