diff --git a/libs/twitch-domain/src/lib/aggregates/viewer.aggregate.ts b/libs/twitch-domain/src/lib/aggregates/viewer.aggregate.ts index 420d67de..8a6b47b4 100644 --- a/libs/twitch-domain/src/lib/aggregates/viewer.aggregate.ts +++ b/libs/twitch-domain/src/lib/aggregates/viewer.aggregate.ts @@ -5,13 +5,14 @@ import { Nullable } from '@toxictoast/azkaban-base-types'; export class ViewerAggregate implements Domain { constructor( private readonly id: string, - private user_id: string, - private login: string, - private display_name: string, - private type: string, - private profile_image_url: string, - private offline_image_url: string, - private view_count: number, + private readonly display_name: string, + private joins: number, + private parts: number, + private messages: number, + private timeouts: number, + private bans: number, + private minutes_watched: number, + private lastseen_at: Nullable, private readonly created_at: Date, private updated_at: Nullable, private deleted_at: Nullable, @@ -36,13 +37,14 @@ export class ViewerAggregate implements Domain { toAnemic(): ViewerAnemic { return { id: this.id, - user_id: this.user_id, - login: this.login, display_name: this.display_name, - type: this.type, - profile_image_url: this.profile_image_url, - offline_image_url: this.offline_image_url, - view_count: this.view_count, + lastseen_at: this.lastseen_at, + joins: this.joins, + parts: this.parts, + messages: this.messages, + timeouts: this.timeouts, + bans: this.bans, + minutes_watched: this.minutes_watched, created_at: this.created_at, updated_at: this.updated_at, deleted_at: this.deleted_at, @@ -51,52 +53,52 @@ export class ViewerAggregate implements Domain { }; } - updateUserId(user_id: string): void { - if (this.user_id !== user_id) { + updateLastSeenAt(lastseen_at: Nullable): void { + if (this.lastseen_at !== lastseen_at) { this.updated_at = new Date(); - this.user_id = user_id; + this.lastseen_at = lastseen_at; } } - updateLogin(login: string): void { - if (this.login !== login) { + updateJoins(joins: number): void { + if (this.joins !== joins) { this.updated_at = new Date(); - this.login = login; + this.joins = joins; } } - updateDisplayName(display_name: string): void { - if (this.display_name !== display_name) { + updateParts(parts: number): void { + if (this.parts !== parts) { this.updated_at = new Date(); - this.display_name = display_name; + this.parts = parts; } } - updateType(type: string): void { - if (this.type !== type) { + updateMessages(messages: number): void { + if (this.messages !== messages) { this.updated_at = new Date(); - this.type = type; + this.messages = messages; } } - updateProfileImageUrl(profile_image_url: string): void { - if (this.profile_image_url !== profile_image_url) { + updateTimeouts(timeouts: number): void { + if (this.timeouts !== timeouts) { this.updated_at = new Date(); - this.profile_image_url = profile_image_url; + this.timeouts = timeouts; } } - updateOfflineImageUrl(offline_image_url: string): void { - if (this.offline_image_url !== offline_image_url) { + updateBans(bans: number): void { + if (this.bans !== bans) { this.updated_at = new Date(); - this.offline_image_url = offline_image_url; + this.bans = bans; } } - updateViewCount(view_count: number): void { - if (this.view_count !== view_count) { + updateMinutesWatched(minutes_watched: number): void { + if (this.minutes_watched !== minutes_watched) { this.updated_at = new Date(); - this.view_count = view_count; + this.minutes_watched = minutes_watched; } } } diff --git a/libs/twitch-domain/src/lib/anemics/viewer.anemic.ts b/libs/twitch-domain/src/lib/anemics/viewer.anemic.ts index 31caf583..76c7c4f8 100644 --- a/libs/twitch-domain/src/lib/anemics/viewer.anemic.ts +++ b/libs/twitch-domain/src/lib/anemics/viewer.anemic.ts @@ -1,11 +1,13 @@ import { Anemic } from '@toxictoast/azkaban-base-domain'; +import { Nullable } from '@toxictoast/azkaban-base-types'; export interface ViewerAnemic extends Anemic { - readonly user_id: string; - readonly login: string; readonly display_name: string; - readonly type: string; - readonly profile_image_url: string; - readonly offline_image_url: string; - readonly view_count: number; + readonly lastseen_at: Nullable; + readonly joins: number; + readonly parts: number; + readonly messages: number; + readonly timeouts: number; + readonly bans: number; + readonly minutes_watched: number; } diff --git a/libs/twitch-domain/src/lib/data/viewer.data.ts b/libs/twitch-domain/src/lib/data/viewer.data.ts index 5ada8575..28f5e243 100644 --- a/libs/twitch-domain/src/lib/data/viewer.data.ts +++ b/libs/twitch-domain/src/lib/data/viewer.data.ts @@ -1,9 +1,3 @@ export interface ViewerData { - readonly user_id: string; - readonly login: string; readonly display_name: string; - readonly type: string; - readonly profile_image_url: string; - readonly offline_image_url: string; - readonly view_count: number; } diff --git a/libs/twitch-domain/src/lib/factories/index.ts b/libs/twitch-domain/src/lib/factories/index.ts index cb0ff5c3..70853643 100644 --- a/libs/twitch-domain/src/lib/factories/index.ts +++ b/libs/twitch-domain/src/lib/factories/index.ts @@ -1 +1 @@ -export {}; +export * from './viewer.factory'; diff --git a/libs/twitch-domain/src/lib/factories/viewer.factory.ts b/libs/twitch-domain/src/lib/factories/viewer.factory.ts new file mode 100644 index 00000000..f699e7fb --- /dev/null +++ b/libs/twitch-domain/src/lib/factories/viewer.factory.ts @@ -0,0 +1,96 @@ +import { Factory } from '@toxictoast/azkaban-base-domain'; +import { ViewerAnemic } from '../anemics'; +import { ViewerAggregate } from '../aggregates'; +import { ViewerData } from '../data'; +import { ViewerId } from '../valueObjects'; + +export class ViewerFactory + implements Factory +{ + reconstitute(data: ViewerAnemic): ViewerAggregate { + const { + id, + display_name, + lastseen_at, + joins, + parts, + messages, + timeouts, + bans, + minutes_watched, + created_at, + updated_at, + deleted_at, + } = data; + + return new ViewerAggregate( + id, + display_name, + joins, + parts, + messages, + timeouts, + bans, + minutes_watched, + lastseen_at, + created_at, + updated_at, + deleted_at, + ); + } + + constitute(data: ViewerAggregate): ViewerAnemic { + const { + id, + display_name, + lastseen_at, + joins, + parts, + messages, + timeouts, + bans, + minutes_watched, + created_at, + updated_at, + deleted_at, + isUpdated, + isDeleted, + } = data.toAnemic(); + + return { + id, + display_name, + lastseen_at, + joins, + parts, + messages, + timeouts, + bans, + minutes_watched, + created_at, + updated_at, + deleted_at, + isUpdated, + isDeleted, + }; + } + + createDomain(data: ViewerData): ViewerAggregate { + const { display_name } = data; + const viewerId = new ViewerId(); + return new ViewerAggregate( + viewerId.value, + display_name, + 0, + 0, + 0, + 0, + 0, + 0, + new Date(), + new Date(), + null, + null, + ); + } +} diff --git a/libs/twitch-domain/src/lib/repositories/index.ts b/libs/twitch-domain/src/lib/repositories/index.ts index cb0ff5c3..e6ce667c 100644 --- a/libs/twitch-domain/src/lib/repositories/index.ts +++ b/libs/twitch-domain/src/lib/repositories/index.ts @@ -1 +1 @@ -export {}; +export * from './viewer.repository'; diff --git a/libs/twitch-domain/src/lib/repositories/viewer.repository.ts b/libs/twitch-domain/src/lib/repositories/viewer.repository.ts new file mode 100644 index 00000000..bf371d99 --- /dev/null +++ b/libs/twitch-domain/src/lib/repositories/viewer.repository.ts @@ -0,0 +1,12 @@ +import { ViewerAnemic } from '../anemics'; +import { Repository } from '@toxictoast/azkaban-base-domain'; +import { Chainable } from '@toxictoast/azkaban-base-types'; + +interface ViewerAdditions { + findByDisplayName(display_name: string): Promise; +} + +export type ViewerRepository = Chainable< + ViewerAdditions, + Repository +>; diff --git a/libs/twitch-domain/src/lib/services/index.ts b/libs/twitch-domain/src/lib/services/index.ts index cb0ff5c3..7cafc11b 100644 --- a/libs/twitch-domain/src/lib/services/index.ts +++ b/libs/twitch-domain/src/lib/services/index.ts @@ -1 +1 @@ -export {}; +export * from './viewer.service'; diff --git a/libs/twitch-domain/src/lib/services/viewer.service.ts b/libs/twitch-domain/src/lib/services/viewer.service.ts new file mode 100644 index 00000000..275bc690 --- /dev/null +++ b/libs/twitch-domain/src/lib/services/viewer.service.ts @@ -0,0 +1,227 @@ +import { ViewerFactory } from '../factories'; +import { ViewerRepository } from '../repositories'; +import { ViewerAnemic } from '../anemics'; +import { Result } from '@toxictoast/azkaban-base-domain'; +import { Nullable, Optional } from '@toxictoast/azkaban-base-types'; +import { GenericErrorCodes } from '@toxictoast/azkaban-base-helpers'; +import { ViewerData } from '../data'; + +export class ViewerService { + private readonly factory: ViewerFactory = new ViewerFactory(); + + constructor(private readonly repository: ViewerRepository) {} + + private async save(anemic: ViewerAnemic): Promise> { + try { + const result = await this.repository.save(anemic); + return Result.ok(result); + } catch (error) { + return Result.fail(error); + } + } + + async getViewers( + limit?: Optional, + offset?: Optional, + ): Promise>> { + try { + const result = await this.repository.findList(limit, offset); + return Result.ok>(result); + } catch (error) { + return Result.fail>(error); + } + } + + async getViewerById(id: string): Promise> { + try { + const result = await this.repository.findById(id); + if (result !== null) { + return Result.ok(result); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async getViewerByDisplayName( + display_name: string, + ): Promise> { + try { + const result = + await this.repository.findByDisplayName(display_name); + if (result !== null) { + return Result.ok(result); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async createViewer(data: ViewerData): Promise> { + try { + const check = await this.getViewerByDisplayName(data.display_name); + if (check.isSuccess) { + return Result.fail(GenericErrorCodes.UNKNOWN); + } + const aggregate = this.factory.createDomain(data); + return await this.save(aggregate.toAnemic()); + } catch (error) { + return Result.fail(error); + } + } + + async deleteViewer(id: string): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.delete(); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async restoreViewer(id: string): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.restore(); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async updateLastSeenAt( + id: string, + lastseen_at: Nullable, + ): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.updateLastSeenAt(lastseen_at); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async updateJoins( + id: string, + joins: number, + ): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.updateJoins(joins); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async updateParts( + id: string, + parts: number, + ): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.updateParts(parts); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async updateMessages( + id: string, + messages: number, + ): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.updateMessages(messages); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async updateTimeouts( + id: string, + timeouts: number, + ): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.updateTimeouts(timeouts); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async updateBans(id: string, bans: number): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.updateBans(bans); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } + + async updateMinutesWatched( + id: string, + minutes_watched: number, + ): Promise> { + try { + const viewer = await this.getViewerById(id); + if (viewer.isSuccess) { + const viewerValue = viewer.value; + const aggregate = this.factory.reconstitute(viewerValue); + aggregate.updateMinutesWatched(minutes_watched); + return await this.save(aggregate.toAnemic()); + } + return Result.fail(GenericErrorCodes.NOT_FOUND); + } catch (error) { + return Result.fail(error); + } + } +} diff --git a/libs/twitch-domain/src/lib/valueObjects/index.ts b/libs/twitch-domain/src/lib/valueObjects/index.ts index cb0ff5c3..bfdcea91 100644 --- a/libs/twitch-domain/src/lib/valueObjects/index.ts +++ b/libs/twitch-domain/src/lib/valueObjects/index.ts @@ -1 +1 @@ -export {}; +export * from './viewerId.valueObject'; diff --git a/libs/twitch-domain/src/lib/valueObjects/viewerId.valueObject.ts b/libs/twitch-domain/src/lib/valueObjects/viewerId.valueObject.ts new file mode 100644 index 00000000..305f0e79 --- /dev/null +++ b/libs/twitch-domain/src/lib/valueObjects/viewerId.valueObject.ts @@ -0,0 +1,19 @@ +import { ValueObject } from '@toxictoast/azkaban-base-domain'; +import { Nullable, Optional } from '@toxictoast/azkaban-base-types'; +import { UuidHelper } from '@toxictoast/azkaban-base-helpers'; + +export class ViewerId implements ValueObject { + readonly _value: Nullable; + + constructor(value?: Optional) { + this._value = value ?? UuidHelper.create().value; + } + + equals(valueObject: ViewerId): boolean { + return this._value === valueObject._value; + } + + get value(): string { + return this._value; + } +}