Skip to content
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
1 change: 1 addition & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
run: |
npm install
npm run build --if-present
npm run lint
npm test
env:
CI: true
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"test": "npm run test:unit && npm run test:features",
"test:unit": "NODE_ENV=test jest",
"test:features": "NODE_ENV=test cucumber-js -p default",
"lint": "tslint src/**/*.ts{,x}",
"start": "NODE_ENV=production node dist/src/apps/mooc_backend/server",
"build": "npm run build:clean && npm run build:tsc && npm run build:di",
"build:tsc": "tsc -p tsconfig.prod.json",
Expand Down
14 changes: 10 additions & 4 deletions src/Contexts/Mooc/Courses/application/CourseCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { CourseName } from '../domain/CourseName';
import { CourseDuration } from '../domain/CourseDuration';
import { EventBus } from '../../../Shared/domain/EventBus';

type Params = {
courseId: CourseId;
courseName: CourseName;
courseDuration: CourseDuration;
};

export class CourseCreator {
private repository: CourseRepository;
private eventBus: EventBus;
Expand All @@ -15,11 +21,11 @@ export class CourseCreator {
this.eventBus = eventBus;
}

async run(request: CreateCourseRequest): Promise<void> {
async run({ courseId, courseName, courseDuration }: Params): Promise<void> {
const course = Course.create(
new CourseId(request.id),
new CourseName(request.name),
new CourseDuration(request.duration)
courseId,
courseName,
courseDuration
);

await this.repository.save(course);
Expand Down
20 changes: 20 additions & 0 deletions src/Contexts/Mooc/Courses/application/CreateCourseCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Command } from '../../../Shared/domain/Command';

type Params = {
id: string;
name: string;
duration: string;
};

export class CreateCourseCommand extends Command {
id: string;
name: string;
duration: string;

constructor({ id, name, duration }: Params) {
super();
this.id = id;
this.name = name;
this.duration = duration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CreateCourseCommand } from './CreateCourseCommand';
import { CommandHandler } from '../../../Shared/domain/CommandHandler';
import { CourseCreator } from './CourseCreator';
import { Command } from '../../../Shared/domain/Command';
import { CourseId } from '../../Shared/domain/Courses/CourseId';
import { CourseName } from '../domain/CourseName';
import { CourseDuration } from '../domain/CourseDuration';

export class CreateCourseCommandHandler implements CommandHandler<CreateCourseCommand> {
constructor(private courseCreator: CourseCreator) {}

subscribedTo(): Command {
return CreateCourseCommand;
}

async handle(command: CreateCourseCommand): Promise<void> {
const courseId = new CourseId(command.id);
const courseName = new CourseName(command.name);
const courseDuration = new CourseDuration(command.duration);
await this.courseCreator.run({ courseId, courseName, courseDuration });
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CoursesCounterRepository } from '../../domain/CoursesCounterRepository';
import { CoursesCounterNotExist } from '../../domain/CoursesCounterNotExist';
import { CoursesCounterResponse } from './CoursesCounterResponse';
import { FindCoursesCounterResponse } from './FindCoursesCounterResponse';

export class CoursesCounterFinder {
constructor(private repository: CoursesCounterRepository) {}
Expand All @@ -11,6 +11,6 @@ export class CoursesCounterFinder {
throw new CoursesCounterNotExist();
}

return new CoursesCounterResponse(counter.total.value);
return new FindCoursesCounterResponse(counter.total.value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Query } from '../../../../Shared/domain/Query';

export class FindCoursesCounterQuery implements Query {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { QueryHandler } from '../../../../Shared/domain/QueryHandler';
import { FindCoursesCounterQuery } from './FindCoursesCounterQuery';
import { FindCoursesCounterResponse } from './FindCoursesCounterResponse';
import { Query } from '../../../../Shared/domain/Query';
import { CoursesCounterFinder } from './CoursesCounterFinder';

export class FindCoursesCounterQueryHandler
implements QueryHandler<FindCoursesCounterQuery, FindCoursesCounterResponse> {
constructor(private finder: CoursesCounterFinder) {}

subscribedTo(): Query {
return FindCoursesCounterQuery;
}
handle(_query: FindCoursesCounterQuery): Promise<FindCoursesCounterResponse> {
return this.finder.run();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export class CoursesCounterResponse {
export class FindCoursesCounterResponse {
readonly total: number;

constructor(total: number) {
Expand Down
1 change: 1 addition & 0 deletions src/Contexts/Shared/domain/Command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export abstract class Command {}
5 changes: 5 additions & 0 deletions src/Contexts/Shared/domain/CommandBus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Command } from './Command';

export interface CommandBus {
dispatch(command: Command): Promise<void>;
}
6 changes: 6 additions & 0 deletions src/Contexts/Shared/domain/CommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Command } from './Command';

export interface CommandHandler<T extends Command> {
subscribedTo(): Command;
handle(command: T): Promise<void>;
}
Comment on lines +3 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍😊

7 changes: 7 additions & 0 deletions src/Contexts/Shared/domain/CommandNotRegisteredError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Command } from './Command';

export class CommandNotRegisteredError extends Error {
constructor(command: Command) {
super(`The command <${command.constructor.name}> hasn't a command handler associated`);
}
}
1 change: 1 addition & 0 deletions src/Contexts/Shared/domain/Query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export abstract class Query {}
6 changes: 6 additions & 0 deletions src/Contexts/Shared/domain/QueryBus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Query } from './Query';
import { Response } from './Response';

export interface QueryBus {
ask<R extends Response>(query: Query): Promise<R>;
}
7 changes: 7 additions & 0 deletions src/Contexts/Shared/domain/QueryHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Query } from './Query';
import { Response } from './Response';

export interface QueryHandler<Q extends Query, R extends Response> {
subscribedTo(): Query;
handle(query: Q): Promise<R>;
}
7 changes: 7 additions & 0 deletions src/Contexts/Shared/domain/QueryNotRegisteredError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Query } from './Query';

export class QueryNotRegisteredError extends Error {
constructor(query: Query) {
super(`The query <${query.constructor.name}> hasn't a query handler associated`);
}
}
1 change: 1 addition & 0 deletions src/Contexts/Shared/domain/Response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface Response {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Command } from '../../domain/Command';
import { CommandHandler } from '../../domain/CommandHandler';
import { CommandNotRegisteredError } from '../../domain/CommandNotRegisteredError';

export class CommandHandlersInformation {
private commandHandlersMap: Map<Command, CommandHandler<Command>>;

constructor(commandHandlers: Array<CommandHandler<Command>>) {
this.commandHandlersMap = this.formatHandlers(commandHandlers);
}

private formatHandlers(commandHandlers: Array<CommandHandler<Command>>): Map<Command, CommandHandler<Command>> {
const handlersMap = new Map();

commandHandlers.forEach(commandHandler => {
handlersMap.set(commandHandler.subscribedTo(), commandHandler);
});

return handlersMap;
}

public search(command: Command): CommandHandler<Command> {
const commandHandler = this.commandHandlersMap.get(command.constructor);

if (!commandHandler) {
throw new CommandNotRegisteredError(command);
}

return commandHandler;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Command } from '../../domain/Command';
import { CommandBus } from './../../domain/CommandBus';
import { CommandHandlersInformation } from './CommandHandlersInformation';

export class InMemoryCommandBus implements CommandBus {
constructor(private commandHandlersInformation: CommandHandlersInformation) {}

async dispatch(command: Command): Promise<void> {
const handler = this.commandHandlersInformation.search(command);

await handler.handle(command);
}
}
14 changes: 14 additions & 0 deletions src/Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Query } from '../../domain/Query';
import { Response } from '../../domain/Response';
import { QueryBus } from './../../domain/QueryBus';
import { QueryHandlersInformation } from './QueryHandlersInformation';

export class InMemoryQueryBus implements QueryBus {
constructor(private queryHandlersInformation: QueryHandlersInformation) {}

async ask<R extends Response>(query: Query): Promise<R> {
const handler = this.queryHandlersInformation.search(query);

return handler.handle(query) as Promise<R>;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Query } from '../../domain/Query';
import { QueryHandler } from '../../domain/QueryHandler';
import { Response } from '../../domain/Response';
import { QueryNotRegisteredError } from '../../domain/QueryNotRegisteredError';

export class QueryHandlersInformation {
private queryHandlersMap: Map<Query, QueryHandler<Query, Response>>;

constructor(queryHandlers: Array<QueryHandler<Query, Response>>) {
this.queryHandlersMap = this.formatHandlers(queryHandlers);
}

private formatHandlers(
queryHandlers: Array<QueryHandler<Query, Response>>
): Map<Query, QueryHandler<Query, Response>> {
const handlersMap = new Map();

queryHandlers.forEach(queryHandler => {
handlersMap.set(queryHandler.subscribedTo(), queryHandler);
});

return handlersMap;
}

public search(query: Query): QueryHandler<Query, Response> {
const queryHandler = this.queryHandlersMap.get(query.constructor);

if (!queryHandler) {
throw new QueryNotRegisteredError(query);
}
Comment on lines +28 to +30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we handling these exception somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception and the CommandNotRegisteredError are handled by the controller, throwing a 500 error. Is not handled as a specific error but its a general error.

https://github.com/CodelyTV/typescript-ddd-skeleton/blob/d6b7729e96f7ef64681101151e794f38913ed9d9/src/apps/mooc_backend/controllers/CoursesCounterGetController.ts#L21


return queryHandler;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ services:
Mooc.courses.CourseCreator:
class: ../../../../../Contexts/Mooc/Courses/application/CourseCreator
arguments: ['@Mooc.courses.CourseRepository', '@Mooc.shared.EventBus']

Mooc.courses.CreateCourseCommandHandler:
class: ../../../../../Contexts/Mooc/Courses/application/CreateCourseCommandHandler
arguments: ['@Mooc.courses.CourseCreator']
tags:
- { name: 'commandHandler' }
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
services:

Mooc.coursesCounter.CoursesCounterRepository:
class: ../../../../../Contexts/Mooc/CoursesCounter/infrastructure/persistence/mongo/MongoCoursesCounterRepository
arguments: ["@Mooc.shared.ConnectionManager"]
Expand All @@ -21,4 +20,10 @@ services:
class: ../../../../../Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder
arguments: ["@Mooc.coursesCounter.CoursesCounterRepository"]


Mooc.coursesCounter.FindCoursesCounterQueryHandler:
class: ../../../../../Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQueryHandler
arguments: ["@Mooc.coursesCounter.CoursesCounterFinder"]
tags:
- { name: 'queryHandler' }

Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,26 @@ services:
class: ../../../../../Contexts/Shared/infrastructure/EventBus/InMemoryAsyncEventBus
arguments: []

Mooc.shared.CommandHandlersInformation:
class: ../../../../../Contexts/Shared/infrastructure/CommandBus/CommandHandlersInformation
arguments: ['!tagged commandHandler']

Mooc.shared.CommandBus:
class: ../../../../../Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus
arguments: ['@Mooc.shared.CommandHandlersInformation']

Mooc.shared.EventBus.DomainEventMapping:
class: ../../../../../Contexts/Shared/infrastructure/EventBus/DomainEventMapping
arguments: ['!tagged domainEventSubscriber']

Mooc.shared.EventBus.DomainEventJsonDeserializer:
class: ../../../../../Contexts/Shared/infrastructure/EventBus/DomainEventJsonDeserializer
arguments: ['@Mooc.shared.EventBus.DomainEventMapping']

Mooc.shared.QueryHandlersInformation:
class: ../../../../../Contexts/Shared/infrastructure/QueryBus/QueryHandlersInformation
arguments: ['!tagged queryHandler']

Mooc.shared.QueryBus:
class: ../../../../../Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus
arguments: ['@Mooc.shared.QueryHandlersInformation']
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ services:

Apps.mooc.controllers.CoursePutController:
class: ../../../controllers/CoursePutController
arguments: ["@Mooc.courses.CourseCreator"]
arguments: ["@Mooc.shared.CommandBus"]

Apps.mooc.controllers.StatusGetController:
class: ../../../controllers/StatusGetController
arguments: []

Apps.mooc.controllers.CoursesCounterGetController:
class: ../../../controllers/CoursesCounterGetController
arguments: ["@Mooc.coursesCounter.CoursesCounterFinder"]
arguments: ["@Mooc.shared.QueryBus"]
8 changes: 5 additions & 3 deletions src/apps/mooc_backend/controllers/CoursePutController.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { Request, Response } from 'express';
import { CourseCreator } from '../../../Contexts/Mooc/Courses/application/CourseCreator';
import httpStatus from 'http-status';
import { Controller } from './Controller';
import { CourseAlreadyExists } from '../../../Contexts/Mooc/Courses/domain/CourseAlreadyExists';
import { CommandBus } from '../../../Contexts/Shared/domain/CommandBus';
import { CreateCourseCommand } from '../../../Contexts/Mooc/Courses/application/CreateCourseCommand';

export class CoursePutController implements Controller {
constructor(private courseCreator: CourseCreator) {}
constructor(private commandBus: CommandBus) {}

async run(req: Request, res: Response) {
const id: string = req.params.id;
const name: string = req.body.name;
const duration: string = req.body.duration;
const createCourseCommand = new CreateCourseCommand({ id, name, duration });

try {
await this.courseCreator.run({ id, name, duration });
await this.commandBus.dispatch(createCourseCommand);
} catch (error) {
if (error instanceof CourseAlreadyExists) {
res.status(httpStatus.BAD_REQUEST).send(error.message);
Expand Down
12 changes: 8 additions & 4 deletions src/apps/mooc_backend/controllers/CoursesCounterGetController.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Controller } from './Controller';
import { Request, Response } from 'express';
import { CoursesCounterFinder } from '../../../Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder';
import httpStatus = require('http-status');
import { CoursesCounterNotExist } from '../../../Contexts/Mooc/CoursesCounter/domain/CoursesCounterNotExist';
import { FindCoursesCounterQuery } from '../../../Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQuery';
import { QueryBus } from '../../../Contexts/Shared/domain/QueryBus';
import { FindCoursesCounterResponse } from '../../../Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterResponse';

export class CoursesCounterGetController implements Controller {
constructor(private coursesCounterFinder: CoursesCounterFinder) {}
constructor(private queryBus: QueryBus) {}
async run(req: Request, res: Response): Promise<void> {
try {
const counter = await this.coursesCounterFinder.run();
res.status(httpStatus.OK).send(counter);
const query = new FindCoursesCounterQuery();
const count = await this.queryBus.ask<FindCoursesCounterResponse>(query);

res.status(httpStatus.OK).send(count);
} catch (e) {
if (e instanceof CoursesCounterNotExist) {
res.status(httpStatus.NOT_FOUND).send();
Expand Down
Loading