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
24 changes: 12 additions & 12 deletions packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ datasource db {
}

generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["interactiveTransactions"]
}

Expand All @@ -23,26 +23,26 @@ model AuthUser {
}

model RegisteredEmails {
userId String @id
email String @unique
userId String @id
email String @unique
}

model LearningMaterials {
id String @id
url String
courseUserId String @unique
id String @id
url String
courseUserId String @unique
}

model Event {
id String @id
id String @id
type String
streamId String
streamCategory String
streamVersion Int
occurredAt DateTime
data Json
metadata Json
globalOrder Int @default(autoincrement())
globalOrder Int @default(autoincrement())
}

model Course {
Expand All @@ -57,8 +57,8 @@ enum Role {
}

model CourseProgress {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
courseUserId String @unique
learningMaterialsId String @unique
learningMaterialsCompletedTasks Int
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
courseUserId String? @unique
learningMaterialsId String @unique
learningMaterialsCompletedTasks Int
}
8 changes: 7 additions & 1 deletion packages/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CourseProgressReadModule } from '@/read/course-progress/course-progress
import { LearningMaterialsReadModule } from '@/read/learning-materials/learning-materials.read-module';
import { env } from '@/shared/env';
import { EmailConfirmationWriteModule } from '@/write/email-confirmation/email-confirmation.write-module';
import { LearningMaterialsTasksModule } from '@/write/learning-materials-tasks/learning-materials-tasks.write-module';
import { LearningMaterialsUrlWriteModule } from '@/write/learning-materials-url/learning-materials-url.write-module';
import { UserRegistrationWriteModule } from '@/write/user-registration/user-registration.write-module';

Expand All @@ -19,7 +20,12 @@ import { CoursesModule } from './crud/courses/courses.module';

const isProduction = env.NODE_ENV === 'production';

const writeModules = [LearningMaterialsUrlWriteModule, UserRegistrationWriteModule, EmailConfirmationWriteModule];
const writeModules = [
LearningMaterialsUrlWriteModule,
UserRegistrationWriteModule,
LearningMaterialsTasksModule,
EmailConfirmationWriteModule,
];
const readModules = [LearningMaterialsReadModule, CourseProgressReadModule];
const automationModules = [SendEmailWhenLearningMaterialsUrlWasGeneratedAutomationModule];
const eventModelingModules = [...writeModules, ...readModules, ...automationModules];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,40 +177,4 @@ describe('Read Slice | CourseProgress', () => {
},
});
});

it('when taskWasUnCompleted and learningMaterialsCompletedTasks is equal to 0 then learningMaterialsCompletedTasks should be 0', async () => {
// Given
const { id, courseUserId, learningMaterialsId, initialLearningMaterialCompletedTask } = givenData(uuid());

// When
await moduleUnderTest.eventOccurred(
learningMaterialsUrlWasGeneratedWithId(id),
EventStreamName.from('LearningMaterialsUrl', courseUserId),
);

// Then
await moduleUnderTest.expectReadModel({
learningMaterialsId,
readModel: {
learningMaterialsId,
courseUserId,
learningMaterialsCompletedTasks: initialLearningMaterialCompletedTask,
},
});

// When
await moduleUnderTest.eventOccurred(
statusTask(learningMaterialsId, 'TaskWasUncompleted'),
EventStreamName.from('LearningMaterialsTasks', courseUserId),
);
// Then
await moduleUnderTest.expectReadModel({
learningMaterialsId,
readModel: {
learningMaterialsId,
courseUserId,
learningMaterialsCompletedTasks: 0,
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,44 +18,48 @@ export class CourseProgressReadModule {
constructor(private readonly prismaService: PrismaService) {}

@OnEvent('LearningMaterialsUrl.LearningMaterialsUrlWasGenerated')
async onLearningResourcesUrlWasGenerated(event: ApplicationEvent<LearningMaterialsUrlWasGenerated>) {
await this.prismaService.courseProgress.create({
data: {
courseUserId: event.data.courseUserId,
learningMaterialsId: event.data.learningMaterialsId,
learningMaterialsCompletedTasks: 0,
},
async onLearningResourcesUrlWasGenerated({
data: { learningMaterialsId, courseUserId },
}: ApplicationEvent<LearningMaterialsUrlWasGenerated>) {
await this.prismaService.courseProgress.upsert({
where: { learningMaterialsId },
create: { learningMaterialsId, learningMaterialsCompletedTasks: 0, courseUserId },
update: { courseUserId },
});
}

@OnEvent('LearningMaterialsTasks.TaskWasUncompleted')
async onTaskWasUncompleted(event: ApplicationEvent<TaskWasUncompleted>) {
const where = { learningMaterialsId: event.data.learningMaterialsId };
const courseProgress = await this.prismaService.courseProgress.findUnique({ where });

if (!courseProgress || courseProgress.learningMaterialsCompletedTasks === 0) {
return;
}

await this.prismaService.courseProgress.update({
where,
data: {
learningMaterialsCompletedTasks: { decrement: 1 },
},
});
async onTaskWasUncompleted({ data: { learningMaterialsId } }: ApplicationEvent<TaskWasUncompleted>) {
await this.prismaService.courseProgress.upsert(
this.courseProgressStateUpdated({ learningMaterialsId, completedTasks: 'decrement' }),
);
}

@OnEvent('LearningMaterialsTasks.TaskWasCompleted')
async onTaskWasCompleted(event: ApplicationEvent<TaskWasCompleted>) {
await this.prismaService.courseProgress.update({
where: {
learningMaterialsId: event.data.learningMaterialsId,
},
data: {
async onTaskWasCompleted({ data: { learningMaterialsId } }: ApplicationEvent<TaskWasCompleted>) {
await this.prismaService.courseProgress.upsert(
this.courseProgressStateUpdated({ learningMaterialsId, completedTasks: 'increment' }),
);
}

private courseProgressStateUpdated({
learningMaterialsId,
completedTasks,
}: {
learningMaterialsId: string;
completedTasks: 'increment' | 'decrement';
}) {
return {
where: { learningMaterialsId },
update: {
learningMaterialsCompletedTasks: {
increment: 1,
[completedTasks]: 1,
},
},
});
create: {
learningMaterialsId,
learningMaterialsCompletedTasks: completedTasks === 'increment' ? 1 : 0,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AbstractApplicationCommand } from '@/module/application-command-events';

import { CompleteTask } from './complete-task.domain-command';

export class CompleteTaskApplicationCommand extends AbstractApplicationCommand<CompleteTask> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type CompleteTask = {
type: 'CompleteTask';
data: { learningMaterialsId: string; taskId: string };
};
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 { CompleteTaskApplicationCommand } from '@/commands/complete-task.application-command';
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';

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

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

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

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

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

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

it('should return task was completed', () => {
// Given
const pastEvents: TaskWasCompleted[] = [];

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

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

it('should return task was completed if other tasks are completed', () => {
// Given
const pastEvents: TaskWasCompleted[] = [
{
type: 'TaskWasCompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: 'BIt23CR3dLKkHn_a2IM4V' },
},
];

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

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

it('should throw exception if task was already completed', () => {
// Given
const pastEvents: TaskWasCompleted[] = [
{
type: 'TaskWasCompleted',
data: { learningMaterialsId: command.data.learningMaterialsId, taskId: command.data.taskId },
},
];

// When
const events = () => completeTask(pastEvents, command);

// Then
expect(events).toThrowError('Task was already completed');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TaskWasCompleted } from '@/events/task-was-completed.domain-event';
import { CompleteTask } from '@/module/commands/complete-task.domain-command';

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

if (state.completed) {
throw new Error('Task was already completed');
}

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

return [newEvent];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { initWriteTestModule } from '@/shared/test-utils';

export async function learningMaterialsTasksTestModule() {
return initWriteTestModule();
}
Loading