From 2f69917c9029add0e349f1d17fa1a54a0b6a0f7b Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Sat, 4 Sep 2021 14:27:12 +0200 Subject: [PATCH 01/15] command --- .../shared/commands/complete-task.application-command.ts | 5 +++++ .../module/shared/commands/complete-task.domain-command.ts | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 packages/api/src/module/shared/commands/complete-task.application-command.ts create mode 100644 packages/api/src/module/shared/commands/complete-task.domain-command.ts diff --git a/packages/api/src/module/shared/commands/complete-task.application-command.ts b/packages/api/src/module/shared/commands/complete-task.application-command.ts new file mode 100644 index 00000000..7f91f7a6 --- /dev/null +++ b/packages/api/src/module/shared/commands/complete-task.application-command.ts @@ -0,0 +1,5 @@ +import { AbstractApplicationCommand } from '@/module/application-command-events'; + +import { CompleteTask } from './complete-task.domain-command'; + +export class CompleteTaskApplicationCommand extends AbstractApplicationCommand {} diff --git a/packages/api/src/module/shared/commands/complete-task.domain-command.ts b/packages/api/src/module/shared/commands/complete-task.domain-command.ts new file mode 100644 index 00000000..cecacf33 --- /dev/null +++ b/packages/api/src/module/shared/commands/complete-task.domain-command.ts @@ -0,0 +1,4 @@ +export type CompleteTask = { + type: 'CompleteTask'; + data: { learningMaterialsId: string; taskId: string }; +}; From 49f3feeb02937ecbc89eb420d37d392c94b715c8 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Sun, 5 Sep 2021 19:46:27 +0200 Subject: [PATCH 02/15] complete task function --- .../domain/complete-task.spec.ts | 49 +++++++++++++++++++ .../domain/complete-task.ts | 25 ++++++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts create mode 100644 packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts diff --git a/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts new file mode 100644 index 00000000..4ca3b4ac --- /dev/null +++ b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts @@ -0,0 +1,49 @@ +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', () => { + let command: CompleteTask; + + 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 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'); + }); + + function setup() { + command = { + type: 'CompleteTask', + data: { learningMaterialsId: 'sbAPITNMsl2wW6j2cg1H2A', taskId: 'L9EXtwmBNBXgo_qh0uzbq' }, + }; + } + + beforeEach(setup); +}); diff --git a/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts new file mode 100644 index 00000000..7e28110a --- /dev/null +++ b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts @@ -0,0 +1,25 @@ +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 wasTaskAlreadyCompleted = (events: TaskWasCompleted[], commandTaskId: string): boolean => { + return !!events.find(({ data: { taskId: pastEventTaskId } }) => pastEventTaskId === commandTaskId); + }; + + if (wasTaskAlreadyCompleted(pastEvents, taskId)) { + throw new Error('Task was already completed'); + } + + const newEvent: TaskWasCompleted = { + type: 'TaskWasCompleted', + data: { + taskId, + learningMaterialsId, + }, + }; + + return [newEvent]; +} From f7123021001505090225853377ff37bde41b370d Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Sun, 5 Sep 2021 20:23:06 +0200 Subject: [PATCH 03/15] controller --- .../rest/process-st-events.rest-controller.ts | 28 +++++++++++++++++++ .../types/taskCompletedRequestBody.ts | 12 ++++++++ 2 files changed, 40 insertions(+) create mode 100644 packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts create mode 100644 packages/api/src/module/write/learning-materials-tasks/types/taskCompletedRequestBody.ts diff --git a/packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts b/packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts new file mode 100644 index 00000000..ab77c39e --- /dev/null +++ b/packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts @@ -0,0 +1,28 @@ +import { Body, Controller, HttpCode, Post } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; + +import { CompleteTaskApplicationCommand } from '@/module/commands/complete-task.application-command'; +import { ApplicationCommandFactory } from '@/write/shared/application/application-command.factory'; + +import { TaskCompletedRequestBody } from '../../types/taskCompletedRequestBody'; + +@Controller('process-st/events') +export class LearningMaterialsUrlRestController { + constructor(private readonly commandBus: CommandBus, private readonly commandFactory: ApplicationCommandFactory) {} + + @Post('task-checked-unchecked') + @HttpCode(204) + async taskCheckedUnchecked( + @Body() { id: learningMaterialsId, data: { id: taskId, status } }: TaskCompletedRequestBody, + ): Promise { + if (status === 'Completed') { + const command = this.commandFactory.applicationCommand(() => ({ + class: CompleteTaskApplicationCommand, + type: 'CompleteTask', + data: { learningMaterialsId, taskId }, + })); + + await this.commandBus.execute(command); + } + } +} diff --git a/packages/api/src/module/write/learning-materials-tasks/types/taskCompletedRequestBody.ts b/packages/api/src/module/write/learning-materials-tasks/types/taskCompletedRequestBody.ts new file mode 100644 index 00000000..13c4575c --- /dev/null +++ b/packages/api/src/module/write/learning-materials-tasks/types/taskCompletedRequestBody.ts @@ -0,0 +1,12 @@ +export interface TaskCompletedRequestBody { + id: string; + createdDate: string; + data: { + id: string; + status: string; + stopped: boolean; + hidden: boolean; + name: string; + }; + type: string; +} From 90c14bb19aef0b92a729bf18f452b4a77ddcc38b Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Sun, 5 Sep 2021 20:30:36 +0200 Subject: [PATCH 04/15] command handler --- .../complete-task.command-handler.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/api/src/module/write/learning-materials-tasks/application/complete-task.command-handler.ts diff --git a/packages/api/src/module/write/learning-materials-tasks/application/complete-task.command-handler.ts b/packages/api/src/module/write/learning-materials-tasks/application/complete-task.command-handler.ts new file mode 100644 index 00000000..3bf718c2 --- /dev/null +++ b/packages/api/src/module/write/learning-materials-tasks/application/complete-task.command-handler.ts @@ -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 { + constructor( + @Inject(APPLICATION_SERVICE) + private readonly applicationService: ApplicationService, + ) {} + + async execute(command: CompleteTaskApplicationCommand): Promise { + const eventStream = EventStreamName.from('LearningMaterialsTasks', command.data.learningMaterialsId); + + await this.applicationService.execute( + eventStream, + { + causationId: command.id, + correlationId: command.metadata.correlationId, + }, + (pastEvents) => completeTask(pastEvents, command), + ); + } +} From 22ccd6c4cb4e212314b122cdb92e73e5da42bfc5 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Mon, 6 Sep 2021 00:48:59 +0200 Subject: [PATCH 05/15] module --- packages/api/src/app.module.ts | 3 ++- .../learning-materials-tasks.test-module.ts | 5 +++++ .../learning-materials-tasks.write-module.ts | 12 ++++++++++++ .../rest/process-st-events.rest-controller.ts | 4 ++-- 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.test-module.ts create mode 100644 packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.ts diff --git a/packages/api/src/app.module.ts b/packages/api/src/app.module.ts index aa1235ca..8d635aba 100644 --- a/packages/api/src/app.module.ts +++ b/packages/api/src/app.module.ts @@ -10,6 +10,7 @@ import { PrismaModule } from '@/prisma/prisma.module'; import { CourseProgressReadModule } from '@/read/course-progress/course-progress.read-module'; import { LearningMaterialsReadModule } from '@/read/learning-materials/learning-materials.read-module'; import { env } from '@/shared/env'; +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'; @@ -18,7 +19,7 @@ import { CoursesModule } from './crud/courses/courses.module'; const isProduction = env.NODE_ENV === 'production'; -const writeModules = [LearningMaterialsUrlWriteModule, UserRegistrationWriteModule]; +const writeModules = [LearningMaterialsUrlWriteModule, UserRegistrationWriteModule, LearningMaterialsTasksModule]; const readModules = [LearningMaterialsReadModule, CourseProgressReadModule]; const automationModules = [SendEmailWhenLearningMaterialsUrlWasGeneratedAutomationModule]; const eventModelingModules = [...writeModules, ...readModules, ...automationModules]; diff --git a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.test-module.ts b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.test-module.ts new file mode 100644 index 00000000..92fa2cb6 --- /dev/null +++ b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.test-module.ts @@ -0,0 +1,5 @@ +import { initWriteTestModule } from '@/shared/test-utils'; + +export async function learningMaterialsTasksTestModule() { + return initWriteTestModule((app) => app); +} diff --git a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.ts b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.ts new file mode 100644 index 00000000..acdd9c64 --- /dev/null +++ b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +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], + controllers: [LearningMaterialsTaskRestController], +}) +export class LearningMaterialsTasksModule {} diff --git a/packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts b/packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts index ab77c39e..14502097 100644 --- a/packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts +++ b/packages/api/src/module/write/learning-materials-tasks/presentation/rest/process-st-events.rest-controller.ts @@ -7,11 +7,11 @@ import { ApplicationCommandFactory } from '@/write/shared/application/applicatio import { TaskCompletedRequestBody } from '../../types/taskCompletedRequestBody'; @Controller('process-st/events') -export class LearningMaterialsUrlRestController { +export class LearningMaterialsTaskRestController { constructor(private readonly commandBus: CommandBus, private readonly commandFactory: ApplicationCommandFactory) {} @Post('task-checked-unchecked') - @HttpCode(204) + @HttpCode(200) async taskCheckedUnchecked( @Body() { id: learningMaterialsId, data: { id: taskId, status } }: TaskCompletedRequestBody, ): Promise { From a7f164109e45496ef9c64f933563af99c9344151 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Mon, 6 Sep 2021 15:35:32 +0200 Subject: [PATCH 06/15] module test --- ...rning-materials-tasks.write-module.spec.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts diff --git a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts new file mode 100644 index 00000000..c5bd9b30 --- /dev/null +++ b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts @@ -0,0 +1,85 @@ +import { AsyncReturnType } from 'type-fest'; + +import { CompleteTaskApplicationCommand } from '@/module/commands/complete-task.application-command'; +import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-event'; + +import { CommandBuilder } from '../shared/application/application-command.factory'; +import { EventStreamName } from '../shared/application/event-stream-name.value-object'; +import { learningMaterialsTasksTestModule } from './learning-materials-tasks.test-module'; + +describe('learning materials tasks', () => { + let module: AsyncReturnType; + let commandBuilder: ( + taskId?: string, + learningMaterialsId?: string, + ) => ReturnType>; + let learningMaterialsEventTrigger: (learningMaterialsId?: string) => Promise; + + it('should change state of the task to complete', async () => { + // Given + const command = commandBuilder(); + + // When + await learningMaterialsEventTrigger(); + await module.executeCommand(() => command); + + // Then + await module.expectEventPublishedLastly({ + type: 'TaskWasCompleted', + data: { + learningMaterialsId: command.data.learningMaterialsId, + taskId: command.data.taskId, + }, + streamName: EventStreamName.from('LearningMaterialsTasks', command.data.learningMaterialsId), + }); + }); + + it('should not change task state if task is already completed', async () => { + // Given + const command = commandBuilder(); + + // When + await learningMaterialsEventTrigger(); + await module.executeCommand(() => command); + + // Then + await expect(() => module.executeCommand(() => command)).rejects.toThrow(); + }); + + it.todo("should throw exception if materials wasn't yet generated"); + + beforeEach(async () => { + module = await learningMaterialsTasksTestModule(); + }); + + afterEach(async () => { + await module.close(); + }); + + beforeAll(async () => { + const LEARNING_MATERIALS_ID = 'ZpMpw2eh1llFCGKZJEN6r'; + const COURSE_USER_ID = 'Mbs34f1BTQDHxHv3fb68c'; + + commandBuilder = (taskId = 'VmkxXnPG02CaUNV8Relzk', learningMaterialsId = LEARNING_MATERIALS_ID) => ({ + class: CompleteTaskApplicationCommand, + type: 'CompleteTask', + data: { taskId, learningMaterialsId }, + }); + + learningMaterialsEventTrigger = async (learningMaterialsId = LEARNING_MATERIALS_ID) => { + return module.eventOccurred( + EventStreamName.from('LearningMaterialsUrl', COURSE_USER_ID), + { + type: 'LearningMaterialsUrlWasGenerated', + data: { + learningMaterialsId, + COURSE_USER_ID, + materialsUrl: + 'https://app.process.st/runs/Jan%20Kowalski-sbAPITNMsl2wW6j2cg1H2A/tasks/oFBpTVsw_DS_O5B-OgtHXA', + }, + }, + 0, + ); + }; + }); +}); From 7fac1efdb344eb3eda49105b17bf899269d3eac3 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Mon, 6 Sep 2021 16:29:11 +0200 Subject: [PATCH 07/15] function name --- .../learning-materials-tasks.write-module.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts index c5bd9b30..3807226c 100644 --- a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts +++ b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts @@ -13,14 +13,14 @@ describe('learning materials tasks', () => { taskId?: string, learningMaterialsId?: string, ) => ReturnType>; - let learningMaterialsEventTrigger: (learningMaterialsId?: string) => Promise; + let generateLearningMaterials: (learningMaterialsId?: string) => Promise; it('should change state of the task to complete', async () => { // Given const command = commandBuilder(); // When - await learningMaterialsEventTrigger(); + await generateLearningMaterials(); await module.executeCommand(() => command); // Then @@ -39,7 +39,7 @@ describe('learning materials tasks', () => { const command = commandBuilder(); // When - await learningMaterialsEventTrigger(); + await generateLearningMaterials(); await module.executeCommand(() => command); // Then @@ -66,7 +66,7 @@ describe('learning materials tasks', () => { data: { taskId, learningMaterialsId }, }); - learningMaterialsEventTrigger = async (learningMaterialsId = LEARNING_MATERIALS_ID) => { + generateLearningMaterials = async (learningMaterialsId = LEARNING_MATERIALS_ID) => { return module.eventOccurred( EventStreamName.from('LearningMaterialsUrl', COURSE_USER_ID), { From fd246780837481d09a79c3c2b05d4b052b82f0b1 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Mon, 6 Sep 2021 17:04:36 +0200 Subject: [PATCH 08/15] module test --- .../learning-materials-tasks.write-module.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts index 3807226c..e2b71beb 100644 --- a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts +++ b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts @@ -46,7 +46,7 @@ describe('learning materials tasks', () => { await expect(() => module.executeCommand(() => command)).rejects.toThrow(); }); - it.todo("should throw exception if materials wasn't yet generated"); + it.todo("should throw exception if materials haven't been yet generated"); beforeEach(async () => { module = await learningMaterialsTasksTestModule(); @@ -66,14 +66,14 @@ describe('learning materials tasks', () => { data: { taskId, learningMaterialsId }, }); - generateLearningMaterials = async (learningMaterialsId = LEARNING_MATERIALS_ID) => { + generateLearningMaterials = (learningMaterialsId = LEARNING_MATERIALS_ID) => { return module.eventOccurred( EventStreamName.from('LearningMaterialsUrl', COURSE_USER_ID), { type: 'LearningMaterialsUrlWasGenerated', data: { learningMaterialsId, - COURSE_USER_ID, + courseUserId: COURSE_USER_ID, materialsUrl: 'https://app.process.st/runs/Jan%20Kowalski-sbAPITNMsl2wW6j2cg1H2A/tasks/oFBpTVsw_DS_O5B-OgtHXA', }, From fd43980475d73f47bec5fc18e25721dc8a8924f3 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Mon, 6 Sep 2021 19:20:53 +0200 Subject: [PATCH 09/15] lint:fix --- packages/api/src/app.module.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/api/src/app.module.ts b/packages/api/src/app.module.ts index 00046669..41d373f3 100644 --- a/packages/api/src/app.module.ts +++ b/packages/api/src/app.module.ts @@ -10,8 +10,8 @@ import { PrismaModule } from '@/prisma/prisma.module'; import { CourseProgressReadModule } from '@/read/course-progress/course-progress.read-module'; import { LearningMaterialsReadModule } from '@/read/learning-materials/learning-materials.read-module'; import { env } from '@/shared/env'; -import { LearningMaterialsTasksModule } from '@/write/learning-materials-tasks/learning-materials-tasks.write-module'; 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'; @@ -20,7 +20,12 @@ import { CoursesModule } from './crud/courses/courses.module'; const isProduction = env.NODE_ENV === 'production'; -const writeModules = [LearningMaterialsUrlWriteModule, UserRegistrationWriteModule, LearningMaterialsTasksModule, EmailConfirmationWriteModule]; +const writeModules = [ + LearningMaterialsUrlWriteModule, + UserRegistrationWriteModule, + LearningMaterialsTasksModule, + EmailConfirmationWriteModule, +]; const readModules = [LearningMaterialsReadModule, CourseProgressReadModule]; const automationModules = [SendEmailWhenLearningMaterialsUrlWasGeneratedAutomationModule]; const eventModelingModules = [...writeModules, ...readModules, ...automationModules]; From c6e60ff0d24cc18ae3ad6b7c54e0e5527f590055 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Tue, 7 Sep 2021 16:24:01 +0200 Subject: [PATCH 10/15] Schema change & prettier --- packages/api/prisma/schema.prisma | 24 +++++------ .../course-progress.read-module.ts | 9 +++- ...rning-materials-tasks.write-module.spec.ts | 42 +++---------------- 3 files changed, 24 insertions(+), 51 deletions(-) diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 65c05f51..f40ce554 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -4,7 +4,7 @@ datasource db { } generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" previewFeatures = ["interactiveTransactions"] } @@ -23,18 +23,18 @@ 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 @@ -42,7 +42,7 @@ model Event { occurredAt DateTime data Json metadata Json - globalOrder Int @default(autoincrement()) + globalOrder Int @default(autoincrement()) } model Course { @@ -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? + learningMaterialsId String @unique + learningMaterialsCompletedTasks Int } diff --git a/packages/api/src/module/read/course-progress/course-progress.read-module.ts b/packages/api/src/module/read/course-progress/course-progress.read-module.ts index 3f62bcf1..ce9922ad 100644 --- a/packages/api/src/module/read/course-progress/course-progress.read-module.ts +++ b/packages/api/src/module/read/course-progress/course-progress.read-module.ts @@ -47,15 +47,20 @@ export class CourseProgressReadModule { @OnEvent('LearningMaterialsTasks.TaskWasCompleted') async onTaskWasCompleted(event: ApplicationEvent) { - await this.prismaService.courseProgress.update({ + await this.prismaService.courseProgress.upsert({ where: { learningMaterialsId: event.data.learningMaterialsId, }, - data: { + update: { learningMaterialsCompletedTasks: { increment: 1, }, }, + create: { + courseUserId: null, + learningMaterialsId: event.data.learningMaterialsId, + learningMaterialsCompletedTasks: 1, + }, }); } } diff --git a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts index e2b71beb..72ba47df 100644 --- a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts +++ b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.write-module.spec.ts @@ -3,24 +3,22 @@ import { AsyncReturnType } from 'type-fest'; import { CompleteTaskApplicationCommand } from '@/module/commands/complete-task.application-command'; import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-event'; -import { CommandBuilder } from '../shared/application/application-command.factory'; import { EventStreamName } from '../shared/application/event-stream-name.value-object'; import { learningMaterialsTasksTestModule } from './learning-materials-tasks.test-module'; describe('learning materials tasks', () => { let module: AsyncReturnType; - let commandBuilder: ( - taskId?: string, - learningMaterialsId?: string, - ) => ReturnType>; - let generateLearningMaterials: (learningMaterialsId?: string) => Promise; + const commandBuilder = (taskId = 'VmkxXnPG02CaUNV8Relzk', learningMaterialsId = 'ZpMpw2eh1llFCGKZJEN6r') => ({ + class: CompleteTaskApplicationCommand, + type: 'CompleteTask', + data: { taskId, learningMaterialsId }, + }); it('should change state of the task to complete', async () => { // Given const command = commandBuilder(); // When - await generateLearningMaterials(); await module.executeCommand(() => command); // Then @@ -39,15 +37,12 @@ describe('learning materials tasks', () => { const command = commandBuilder(); // When - await generateLearningMaterials(); await module.executeCommand(() => command); // Then await expect(() => module.executeCommand(() => command)).rejects.toThrow(); }); - it.todo("should throw exception if materials haven't been yet generated"); - beforeEach(async () => { module = await learningMaterialsTasksTestModule(); }); @@ -55,31 +50,4 @@ describe('learning materials tasks', () => { afterEach(async () => { await module.close(); }); - - beforeAll(async () => { - const LEARNING_MATERIALS_ID = 'ZpMpw2eh1llFCGKZJEN6r'; - const COURSE_USER_ID = 'Mbs34f1BTQDHxHv3fb68c'; - - commandBuilder = (taskId = 'VmkxXnPG02CaUNV8Relzk', learningMaterialsId = LEARNING_MATERIALS_ID) => ({ - class: CompleteTaskApplicationCommand, - type: 'CompleteTask', - data: { taskId, learningMaterialsId }, - }); - - generateLearningMaterials = (learningMaterialsId = LEARNING_MATERIALS_ID) => { - return module.eventOccurred( - EventStreamName.from('LearningMaterialsUrl', COURSE_USER_ID), - { - type: 'LearningMaterialsUrlWasGenerated', - data: { - learningMaterialsId, - courseUserId: COURSE_USER_ID, - materialsUrl: - 'https://app.process.st/runs/Jan%20Kowalski-sbAPITNMsl2wW6j2cg1H2A/tasks/oFBpTVsw_DS_O5B-OgtHXA', - }, - }, - 0, - ); - }; - }); }); From 7a5c15fc5300a41332776aa61adcaef0e4788309 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Tue, 7 Sep 2021 16:39:27 +0200 Subject: [PATCH 11/15] fix --- .../read/course-progress/course-progress.rest-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/module/read/course-progress/course-progress.rest-controller.ts b/packages/api/src/module/read/course-progress/course-progress.rest-controller.ts index ec27da0c..d05e008f 100644 --- a/packages/api/src/module/read/course-progress/course-progress.rest-controller.ts +++ b/packages/api/src/module/read/course-progress/course-progress.rest-controller.ts @@ -14,7 +14,7 @@ export class CourseProgressRestController { @Get() async getCourseProgress(@JwtUserId() courseUserId: UserId): Promise { - const courseProgress = await this.prismaService.courseProgress.findUnique({ where: { courseUserId } }); + const courseProgress = await this.prismaService.courseProgress.findFirst({ where: { courseUserId } }); if (!courseProgress) { throw new NotFoundException(); From fdaeef1ec6918955fb91a9d60dd3148d49b279db Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Tue, 7 Sep 2021 21:18:17 +0200 Subject: [PATCH 12/15] move from find to reduce --- .../domain/complete-task.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts index 7e28110a..37a768b6 100644 --- a/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts +++ b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts @@ -5,11 +5,21 @@ export function completeTask( pastEvents: TaskWasCompleted[], { data: { learningMaterialsId, taskId } }: CompleteTask, ): TaskWasCompleted[] { - const wasTaskAlreadyCompleted = (events: TaskWasCompleted[], commandTaskId: string): boolean => { - return !!events.find(({ data: { taskId: pastEventTaskId } }) => pastEventTaskId === commandTaskId); - }; + const state = pastEvents.reduce<{ completed: boolean }>( + (acc, event) => { + switch (event.type) { + case 'TaskWasCompleted': { + return { completed: true }; + } + default: { + return acc; + } + } + }, + { completed: false }, + ); - if (wasTaskAlreadyCompleted(pastEvents, taskId)) { + if (state.completed) { throw new Error('Task was already completed'); } From ca83677bc804b035f07b81e491b72f2086ffc60c Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Tue, 7 Sep 2021 21:28:10 +0200 Subject: [PATCH 13/15] code simplifying --- .../learning-materials-tasks.test-module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.test-module.ts b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.test-module.ts index 92fa2cb6..40621bdb 100644 --- a/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.test-module.ts +++ b/packages/api/src/module/write/learning-materials-tasks/learning-materials-tasks.test-module.ts @@ -1,5 +1,5 @@ import { initWriteTestModule } from '@/shared/test-utils'; export async function learningMaterialsTasksTestModule() { - return initWriteTestModule((app) => app); + return initWriteTestModule(); } From 7cd7be4ea8c7bd2660cdf63a5f847d6fba5200b6 Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Wed, 8 Sep 2021 10:20:04 +0200 Subject: [PATCH 14/15] adjusting to comments --- packages/api/prisma/schema.prisma | 2 +- .../course-progress.read-module.spec.ts | 36 ----------- .../course-progress.read-module.ts | 62 +++++++++---------- .../course-progress.rest-controller.ts | 2 +- .../domain/complete-task.spec.ts | 35 ++++++++--- .../domain/complete-task.ts | 26 ++++---- 6 files changed, 70 insertions(+), 93 deletions(-) diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index f40ce554..b87bb3c2 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -58,7 +58,7 @@ enum Role { model CourseProgress { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - courseUserId String? + courseUserId String? @unique learningMaterialsId String @unique learningMaterialsCompletedTasks Int } diff --git a/packages/api/src/module/read/course-progress/course-progress.read-module.spec.ts b/packages/api/src/module/read/course-progress/course-progress.read-module.spec.ts index f16b4fe8..43d4d1bb 100644 --- a/packages/api/src/module/read/course-progress/course-progress.read-module.spec.ts +++ b/packages/api/src/module/read/course-progress/course-progress.read-module.spec.ts @@ -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, - }, - }); - }); }); diff --git a/packages/api/src/module/read/course-progress/course-progress.read-module.ts b/packages/api/src/module/read/course-progress/course-progress.read-module.ts index ce9922ad..7d16fafb 100644 --- a/packages/api/src/module/read/course-progress/course-progress.read-module.ts +++ b/packages/api/src/module/read/course-progress/course-progress.read-module.ts @@ -18,49 +18,45 @@ export class CourseProgressReadModule { constructor(private readonly prismaService: PrismaService) {} @OnEvent('LearningMaterialsUrl.LearningMaterialsUrlWasGenerated') - async onLearningResourcesUrlWasGenerated(event: ApplicationEvent) { - await this.prismaService.courseProgress.create({ - data: { - courseUserId: event.data.courseUserId, - learningMaterialsId: event.data.learningMaterialsId, - learningMaterialsCompletedTasks: 0, - }, + async onLearningResourcesUrlWasGenerated({ + data: { learningMaterialsId, courseUserId }, + }: ApplicationEvent) { + await this.prismaService.courseProgress.upsert({ + where: { learningMaterialsId }, + create: { learningMaterialsId, learningMaterialsCompletedTasks: 0, courseUserId }, + update: { courseUserId }, }); } @OnEvent('LearningMaterialsTasks.TaskWasUncompleted') - async onTaskWasUncompleted(event: ApplicationEvent) { - 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) { + await this.prismaService.courseProgress.upsert( + this.courseProgressStateUpdateObject({ learningMaterialsId, increment: false }), + ); } @OnEvent('LearningMaterialsTasks.TaskWasCompleted') - async onTaskWasCompleted(event: ApplicationEvent) { - await this.prismaService.courseProgress.upsert({ - where: { - learningMaterialsId: event.data.learningMaterialsId, - }, + async onTaskWasCompleted({ data: { learningMaterialsId } }: ApplicationEvent) { + await this.prismaService.courseProgress.upsert( + this.courseProgressStateUpdateObject({ learningMaterialsId, increment: true }), + ); + } + + private courseProgressStateUpdateObject({ learningMaterialsId, increment }: GenerateCourseProgressUpsertObjectArgs) { + return { + where: { learningMaterialsId }, update: { - learningMaterialsCompletedTasks: { - increment: 1, - }, + learningMaterialsCompletedTasks: increment ? { increment: 1 } : { decrement: 1 }, }, create: { - courseUserId: null, - learningMaterialsId: event.data.learningMaterialsId, - learningMaterialsCompletedTasks: 1, + learningMaterialsId, + learningMaterialsCompletedTasks: increment ? 1 : 0, }, - }); + }; } } + +type GenerateCourseProgressUpsertObjectArgs = { + learningMaterialsId: string; + increment: boolean; +}; diff --git a/packages/api/src/module/read/course-progress/course-progress.rest-controller.ts b/packages/api/src/module/read/course-progress/course-progress.rest-controller.ts index d05e008f..ec27da0c 100644 --- a/packages/api/src/module/read/course-progress/course-progress.rest-controller.ts +++ b/packages/api/src/module/read/course-progress/course-progress.rest-controller.ts @@ -14,7 +14,7 @@ export class CourseProgressRestController { @Get() async getCourseProgress(@JwtUserId() courseUserId: UserId): Promise { - const courseProgress = await this.prismaService.courseProgress.findFirst({ where: { courseUserId } }); + const courseProgress = await this.prismaService.courseProgress.findUnique({ where: { courseUserId } }); if (!courseProgress) { throw new NotFoundException(); diff --git a/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts index 4ca3b4ac..f9e2e2fe 100644 --- a/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts +++ b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.spec.ts @@ -4,7 +4,10 @@ import { TaskWasCompleted } from '@/module/events/task-was-completed.domain-even import { completeTask } from './complete-task'; describe('complete task', () => { - let command: CompleteTask; + const command: CompleteTask = { + type: 'CompleteTask', + data: { learningMaterialsId: 'sbAPITNMsl2wW6j2cg1H2A', taskId: 'L9EXtwmBNBXgo_qh0uzbq' }, + }; it('should return task was completed', () => { // Given @@ -22,6 +25,27 @@ describe('complete task', () => { ]); }); + 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[] = [ @@ -37,13 +61,4 @@ describe('complete task', () => { // Then expect(events).toThrowError('Task was already completed'); }); - - function setup() { - command = { - type: 'CompleteTask', - data: { learningMaterialsId: 'sbAPITNMsl2wW6j2cg1H2A', taskId: 'L9EXtwmBNBXgo_qh0uzbq' }, - }; - } - - beforeEach(setup); }); diff --git a/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts index 37a768b6..0d03642b 100644 --- a/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts +++ b/packages/api/src/module/write/learning-materials-tasks/domain/complete-task.ts @@ -5,19 +5,21 @@ export function completeTask( pastEvents: TaskWasCompleted[], { data: { learningMaterialsId, taskId } }: CompleteTask, ): TaskWasCompleted[] { - const state = pastEvents.reduce<{ completed: boolean }>( - (acc, event) => { - switch (event.type) { - case 'TaskWasCompleted': { - return { completed: true }; + const state = pastEvents + .filter(({ data }) => data.taskId === taskId) + .reduce<{ completed: boolean }>( + (acc, event) => { + switch (event.type) { + case 'TaskWasCompleted': { + return { completed: true }; + } + default: { + return acc; + } } - default: { - return acc; - } - } - }, - { completed: false }, - ); + }, + { completed: false }, + ); if (state.completed) { throw new Error('Task was already completed'); From 81757ace216675400cda61db1439c173e09b9ddb Mon Sep 17 00:00:00 2001 From: Kacper Cyranowski Date: Wed, 8 Sep 2021 12:39:51 +0200 Subject: [PATCH 15/15] courseProgressStateUpdated change --- .../course-progress.read-module.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/api/src/module/read/course-progress/course-progress.read-module.ts b/packages/api/src/module/read/course-progress/course-progress.read-module.ts index 7d16fafb..1a30fc12 100644 --- a/packages/api/src/module/read/course-progress/course-progress.read-module.ts +++ b/packages/api/src/module/read/course-progress/course-progress.read-module.ts @@ -31,32 +31,35 @@ export class CourseProgressReadModule { @OnEvent('LearningMaterialsTasks.TaskWasUncompleted') async onTaskWasUncompleted({ data: { learningMaterialsId } }: ApplicationEvent) { await this.prismaService.courseProgress.upsert( - this.courseProgressStateUpdateObject({ learningMaterialsId, increment: false }), + this.courseProgressStateUpdated({ learningMaterialsId, completedTasks: 'decrement' }), ); } @OnEvent('LearningMaterialsTasks.TaskWasCompleted') async onTaskWasCompleted({ data: { learningMaterialsId } }: ApplicationEvent) { await this.prismaService.courseProgress.upsert( - this.courseProgressStateUpdateObject({ learningMaterialsId, increment: true }), + this.courseProgressStateUpdated({ learningMaterialsId, completedTasks: 'increment' }), ); } - private courseProgressStateUpdateObject({ learningMaterialsId, increment }: GenerateCourseProgressUpsertObjectArgs) { + private courseProgressStateUpdated({ + learningMaterialsId, + completedTasks, + }: { + learningMaterialsId: string; + completedTasks: 'increment' | 'decrement'; + }) { return { where: { learningMaterialsId }, update: { - learningMaterialsCompletedTasks: increment ? { increment: 1 } : { decrement: 1 }, + learningMaterialsCompletedTasks: { + [completedTasks]: 1, + }, }, create: { learningMaterialsId, - learningMaterialsCompletedTasks: increment ? 1 : 0, + learningMaterialsCompletedTasks: completedTasks === 'increment' ? 1 : 0, }, }; } } - -type GenerateCourseProgressUpsertObjectArgs = { - learningMaterialsId: string; - increment: boolean; -};