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
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:

strategy:
matrix:
node-version: [8.x, 10.x, 12.x]
node-version: [12.x, 14.x]
mongodb-version: [4.0, 4.2]

steps:
Expand Down
2 changes: 1 addition & 1 deletion cucumber.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
let common = [
'tests/**/features/*.feature', // Specify our feature files
'tests/**/features/**/*.feature', // Specify our feature files
'--require-module ts-node/register', // Load TypeScript module
'--require tests/**/features/step_definitions/*.steps.ts' // Load step definitions
].join(' ');
Expand Down
7,023 changes: 3,526 additions & 3,497 deletions package-lock.json

Large diffs are not rendered by default.

62 changes: 33 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"name": "typescript-ddd-skeleton",
"version": "1.0.0",
"description": "",
"repository": {
"url": "https://github.com/CodelyTV/typescript-ddd-skeleton"
},
"license": "",
"engines": {
"node": ">=10.15.0",
"npm": ">=6.7.0"
Expand All @@ -18,52 +22,52 @@
"build:clean": "rm -r dist; exit 0"
},
"dependencies": {
"@types/bson": "^4.0.0",
"@types/compression": "^1.0.1",
"@types/convict": "^4.2.1",
"@types/errorhandler": "0.0.32",
"@types/express": "^4.17.2",
"@types/bson": "^4.0.2",
"@types/compression": "^1.7.0",
"@types/convict": "^5.2.1",
"@types/errorhandler": "1.5.0",
"@types/express": "^4.17.6",
"@types/glob": "^7.1.1",
"@types/helmet": "0.0.44",
"@types/mongodb": "^3.3.15",
"@types/node": "~13.1.1",
"@types/uuid": "^3.4.6",
"@types/helmet": "0.0.47",
"@types/mongodb": "^3.5.18",
"@types/node": "^14.0.3",
"@types/uuid": "^8.0.0",
"@types/uuid-validate": "0.0.1",
"body-parser": "^1.19.0",
"bson": "^4.0.2",
"bson": "^4.0.4",
"compression": "^1.7.4",
"convict": "^5.1.0",
"convict": "^6.0.0",
"copy": "^0.3.2",
"errorhandler": "^1.5.1",
"express": "^4.17.1",
"glob": "^7.1.6",
"helmet": "^3.21.2",
"helmet": "^3.22.0",
"http-status": "^1.4.2",
"mandrill-api": "^1.0.45",
"mongodb": "^3.5.2",
"node-dependency-injection": "^2.4.2",
"ts-node": "^8.3.0",
"typescript": "^3.7.2",
"uuid": "^3.3.3",
"mongodb": "^3.5.7",
"node-dependency-injection": "^2.6.3",
"ts-node": "^8.10.1",
"typescript": "^3.9.2",
"uuid": "^8.0.0",
"uuid-validate": "0.0.3",
"winston": "^3.2.1"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.31",
"@types/cucumber": "^4.0.7",
"@types/faker": "^4.1.5",
"@types/jest": "^24.0.18",
"@types/supertest": "^2.0.8",
"@types/aws-lambda": "^8.10.51",
"@types/cucumber": "^6.0.1",
"@types/faker": "^4.1.12",
"@types/jest": "^25.2.3",
"@types/supertest": "^2.0.9",
"cucumber": "^6.0.5",
"faker": "^4.1.0",
"husky": "^1.3.1",
"jest": "^24.9.0",
"lint-staged": "8.2.1",
"prettier": "^1.16.4",
"husky": "^4.2.5",
"jest": "^26.0.1",
"lint-staged": "10.2.4",
"prettier": "^2.0.5",
"supertest": "^4.0.2",
"ts-jest": "^24.1.0",
"ts-node-dev": "^1.0.0-pre.43",
"tslint": "^5.20.1",
"ts-jest": "^26.0.0",
"ts-node-dev": "^1.0.0-pre.44",
"tslint": "^6.1.2",
"tslint-config-prettier": "~1.18.0",
"tslint-eslint-rules": "^5.4.0"
},
Expand Down
3 changes: 1 addition & 2 deletions src/Contexts/Mooc/Courses/application/CourseCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ export class CourseCreator {
}

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

await this.repository.save(course);

this.eventBus.publish(course.pullDomainEvents());
}
}
4 changes: 2 additions & 2 deletions src/Contexts/Mooc/Courses/domain/Course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ export class Course extends AggregateRoot {
return course;
}

static fromPrimitives(plainData: any): Course {
static fromPrimitives(plainData: { id: string; name: string; duration: string }): Course {
return new Course(
new CourseId(plainData.id),
new CourseName(plainData.name),
new CourseDuration(plainData.duration)
);
}

toPrimitives(): any {
toPrimitives() {
return {
id: this.id.value,
name: this.name.value,
Expand Down
8 changes: 4 additions & 4 deletions src/Contexts/Mooc/Courses/domain/CourseCreatedDomainEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export class CourseCreatedDomainEvent extends DomainEvent {

constructor({
id,
eventId,
duration,
name,
duration,
eventId,
occurredOn
}: {
id: string;
Expand All @@ -37,12 +37,12 @@ export class CourseCreatedDomainEvent extends DomainEvent {
};
}

static fromPrimitive(
static fromPrimitives(
aggregateId: string,
body: CreateCourseDomainEventBody,
eventId: string,
occurredOn: Date
): CourseCreatedDomainEvent {
): DomainEvent {
return new CourseCreatedDomainEvent({
id: aggregateId,
duration: body.duration,
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CoursesCounterRepository } from '../../domain/CoursesCounterRepository';
import { CoursesCounterNotExist } from '../../domain/CoursesCounterNotExist';
import { CoursesCounterResponse } from './CoursesCounterResponse';

export class CoursesCounterFinder {
constructor(private repository: CoursesCounterRepository) {}

async run() {
const counter = await this.repository.search();
if (!counter) {
throw new CoursesCounterNotExist();
}

return new CoursesCounterResponse(counter.total.value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class CoursesCounterResponse {
readonly total: number;

constructor(total: number) {
this.total = total;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { EventBus } from '../../../../Shared/domain/EventBus';
import { CourseId } from '../../../Shared/domain/Courses/CourseId';
import { CoursesCounterRepository } from '../../domain/CoursesCounterRepository';
import { CoursesCounter } from '../../domain/CoursesCounter';
import { CoursesCounterId } from '../../domain/CoursesCounterId';

export class CoursesCounterIncrementer {
constructor(private repository: CoursesCounterRepository, private bus: EventBus) {}

async run(courseId: CourseId) {
const counter = (await this.repository.search()) || this.initializeCounter();

if (!counter.hasIncremented(courseId)) {
counter.increment(courseId);

await this.repository.save(counter);
this.bus.publish(counter.pullDomainEvents());
}
}

private initializeCounter(): CoursesCounter {
return CoursesCounter.initialize(CoursesCounterId.random());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { DomainEventClass } from '../../../../Shared/domain/DomainEvent';
import { DomainEventSubscriber } from '../../../../Shared/domain/DomainEventSubscriber';
import { CourseCreatedDomainEvent } from '../../../Courses/domain/CourseCreatedDomainEvent';
import { CourseId } from '../../../Shared/domain/Courses/CourseId';
import { CoursesCounterIncrementer } from './CoursesCounterIncrementer';

export class IncrementCoursesCounterOnCourseCreated implements DomainEventSubscriber<CourseCreatedDomainEvent> {
constructor(private incrementer: CoursesCounterIncrementer) {}

subscribedTo(): DomainEventClass[] {
return [CourseCreatedDomainEvent];
}

async on(domainEvent: CourseCreatedDomainEvent) {
await this.incrementer.run(new CourseId(domainEvent.aggregateId));
}
}
54 changes: 54 additions & 0 deletions src/Contexts/Mooc/CoursesCounter/domain/CoursesCounter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AggregateRoot } from '../../Courses/domain/AggregateRoot';
import { CourseId } from '../../Shared/domain/Courses/CourseId';
import { CoursesCounterTotal } from './CoursesCounterTotal';
import { CoursesCounterId } from './CoursesCounterId';
import { CoursesCounterIncrementedDomainEvent } from './CoursesCounterIncrementedDomainEvent';
import { Uuid } from '../../../Shared/domain/value-object/Uuid';

export class CoursesCounter extends AggregateRoot {
readonly id: CoursesCounterId;
private _total: CoursesCounterTotal;
readonly existingCourses: Array<CourseId>;

constructor(id: CoursesCounterId, total: CoursesCounterTotal, existingCourses?: Array<CourseId>) {
super();
this.id = id;
this._total = total;
this.existingCourses = existingCourses || [];
}

public get total(): CoursesCounterTotal {
return this._total;
}

static initialize(id: Uuid): CoursesCounter {
return new CoursesCounter(id, CoursesCounterTotal.initialize());
}

increment(courseId: CourseId) {
this._total = this.total.increment();
this.existingCourses.push(courseId);
this.record(new CoursesCounterIncrementedDomainEvent({ aggregateId: this.id.value, total: this.total.value }));
}

hasIncremented(courseId: CourseId): boolean {
const exists = this.existingCourses.find(entry => entry.value === courseId.value);
return exists !== undefined;
}

toPrimitives() {
return {
id: this.id.value,
total: this.total.value,
existingCourses: this.existingCourses.map(courseId => courseId.value)
};
}

static fromPrimitives(data: { id: string; total: number; existingCourses: string[] }): CoursesCounter {
return new CoursesCounter(
new CoursesCounterId(data.id),
new CoursesCounterTotal(data.total),
data.existingCourses.map(entry => new CourseId(entry))
);
}
}
3 changes: 3 additions & 0 deletions src/Contexts/Mooc/CoursesCounter/domain/CoursesCounterId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Uuid } from '../../../Shared/domain/value-object/Uuid';

export class CoursesCounterId extends Uuid {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { DomainEvent } from '../../../Shared/domain/DomainEvent';

export class CoursesCounterIncrementedDomainEvent extends DomainEvent {
static readonly EVENT_NAME = 'courses_counter.incremented';
readonly total: number;

constructor(data: { aggregateId: string; total: number; eventId?: string; occurredOn?: Date }) {
super(CoursesCounterIncrementedDomainEvent.EVENT_NAME, data.aggregateId, data.eventId, data.occurredOn);
this.total = data.total;
}

toPrimitive(): Object {
return {
total: this.total
};
}

static fromPrimitive(
aggregateId: string,
body: { total: number },
eventId: string,
occurredOn: Date
): CoursesCounterIncrementedDomainEvent {
return new CoursesCounterIncrementedDomainEvent({
aggregateId,
total: body.total,
eventId,
occurredOn
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class CoursesCounterNotExist extends Error {
constructor() {
super('The courses counter does not exists');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CoursesCounter } from './CoursesCounter';
import { Nullable } from '../../../Shared/domain/Nullable';

export interface CoursesCounterRepository {
search(): Promise<Nullable<CoursesCounter>>;
save(counter: CoursesCounter): Promise<void>;
}
11 changes: 11 additions & 0 deletions src/Contexts/Mooc/CoursesCounter/domain/CoursesCounterTotal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NumberValueObject } from '../../../Shared/domain/value-object/IntValueObject';

export class CoursesCounterTotal extends NumberValueObject {
increment(): CoursesCounterTotal {
return new CoursesCounterTotal(this.value + 1);
}

static initialize(): CoursesCounterTotal {
return new CoursesCounterTotal(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CoursesCounterRepository } from '../domain/CoursesCounterRepository';
import { CoursesCounter } from '../domain/CoursesCounter';
import { CoursesCounterId } from '../domain/CoursesCounterId';
import { CoursesCounterTotal } from '../domain/CoursesCounterTotal';

export class InMemoryCoursesCounterRepository implements CoursesCounterRepository {
private counter: CoursesCounter;
constructor() {
this.counter = new CoursesCounter(CoursesCounterId.random(), new CoursesCounterTotal(0), []);
}

async search(): Promise<CoursesCounter> {
return this.counter;
}

async save(counter: CoursesCounter): Promise<void> {
this.counter = counter;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MongoRepository } from '../../../../../Shared/infrastructure/persistence/mongo/MongoRepository';
import { Nullable } from '../../../../../Shared/domain/Nullable';
import { CoursesCounter } from '../../../domain/CoursesCounter';
import { CoursesCounterRepository } from '../../../domain/CoursesCounterRepository';

export class MongoCoursesCounterRepository extends MongoRepository<CoursesCounter> implements CoursesCounterRepository {
public save(counter: CoursesCounter): Promise<void> {
return this.persist(counter.id.value, counter);
}

public async search(): Promise<Nullable<CoursesCounter>> {
const collection = await this.collection();

const document = await collection.findOne({});
return document ? CoursesCounter.fromPrimitives({ ...document, id: document._id }) : null;
}

protected moduleName(): string {
return 'coursesCounter';
}
}
Loading