Skip to content

Instantly share code, notes, and snippets.

@tcarrio
Last active June 14, 2023 14:16
Show Gist options
  • Save tcarrio/131e6d420f88437597c6335415c7cf10 to your computer and use it in GitHub Desktop.
Save tcarrio/131e6d420f88437597c6335415c7cf10 to your computer and use it in GitHub Desktop.
Domain modeling for D2-Vendor-Alert

domain modeling

This demostrates a scenario of generating persistence-layer agnostic domain models which reflect the context of your service, but are not explicitly tied to a data store (in this case, MongoDB)

The pattern enforces separation of concerns such that the persistence layer and domain layer are kept independent of each other.

One important component here is the interface, IUserRepository, which we could swap with any other implementation backed by whatever data store. You can swap in a Fake for e2e testing that is backed by an in-memory datastore.

// @ts-check
import { User } from '../database/models/users.js'
/**
* Checks if user exists in database
* @param {string} bungieNetUsername User's Bungie Net username
* @returns {Promise<boolean>} true/false
*/
export async function doesUserExist(bungieNetUsername) {
return await User.exists({ bungie_username: bungieNetUsername }).exec() ? true : false
}
/**
* Adds the specified user's information to the database
* @param {string} bungieNetUsername User's Bungie Net username
* @param {string} discordId User's Discord id
* @param {string} discordChannelId Id of Discord channel user initialized the alert from
*/
export async function addUser(bungieNetUsername, discordId, discordChannelId) {
const user = new User({
bungie_username: bungieNetUsername,
discord_id: discordId,
discord_channel_id: discordChannelId
})
try {
await user.save()
} catch (error) {
console.log('Adding user failed')
throw error
}
}
/**
* Updates the database information for a specific user
* @param {string} bungieMembershipId User's membership id on Bungie
* @param {number} refreshExpirationTime Date of expiration for user's refresh token
* @param {string} refreshToken User's refresh token
* @param {string} [bungieNetUsername] User's Bungie username
* @param {string} [destinyId] User's id in Destiny 2
* @param {string} [characterId] User's character (Hunter, Titan, Warlock) id
*/
export async function updateUser(bungieMembershipId, refreshExpirationTime, refreshToken, bungieNetUsername, destinyId, characterId) {
const daysTillTokenExpires = refreshExpirationTime / 60 / 60 / 24
const expirationDate = new Date()
expirationDate.setDate(expirationDate.getDate() + daysTillTokenExpires)
try {
await User.updateOne(
{ bungie_membership_id: bungieMembershipId },
{
$set: {
destiny_id: destinyId,
destiny_character_id: characterId,
refresh_expiration: expirationDate.toISOString(),
refresh_token: refreshToken
}
},
(error) => {
if (error) {
console.log('Updating user record failed')
throw error
} else {
console.log('Updated user record')
}
}
)
} catch (error) {
return error
}
}
// domain models utilizing entities and value objects:
// - entities are globally identifiable and are mutable
// - value objects are non-identifiable other than by their state and are immutable
// they would require some additional work to really make them immutable here, but this demonstrates the premise
export class User {
constructor(
public readonly bungieAccount: BungieAccount,
public readonly destinyCharacter: DestinyCharacter,
public readonly socialConnector: SocialConnector,
) { }
}
export class BungieAccount {
constructor(
public readonly id: string,
public readonly username: string,
public readonly refreshToken: JsonWebToken,
) { }
}
export class JsonWebToken {
constructor(
public readonly header: Map<string, any>,
public readonly payload: Map<string, any>,
public readonly signature: string,
public readonly expiryDate: Date,
public readonly rawToken: string,
) { }
fromRawToken(rawToken: string): JsonWebToken {
// here "jwt" is some helper library for parsing JWTs
const token = jwt.parse(rawToken);
return new JsonWebToken(
token.header,
token.payload,
token.signature,
new Date(token.payload.get('exp')),
rawToken,
);
}
}
export class DestinyCharacter {
constructor(
public readonly id: string,
public readonly accountId: string,
) { }
}
export class SocialConnector {
constructor(
public readonly id: string,
public readonly channelId: string,
) { }
}
// using repository to load a User
interface Logger {
log: (...args: any[]) => void;
debug: (...args: any[]) => void;
error: (...args: any[]) => void;
}
interface MongooseSchema<T> {
exists(criteria: Partial<T>): Command<boolean>;
updateOne(criteria: Partial<T>, operation: MongooseOperation<T>, callback: (error?: Error) => any): Promise<void>;
}
interface MongooseOperation<T> {
$set: Partial<T>;
}
interface Command<T> {
exec(): Promise<T>;
}
interface UserSchema {
bungie_membership_id: string;
bungie_username: string;
destiny_character_id: string;
destiny_id: string;
discord_channel_id: string;
discord_id: string;
refresh_expiration: string;
refresh_token: string;
}
interface PersistenceObject {
save(): Promise<void>;
}
type PersistenceObjectFactory<T> = (model: T) => PersistenceObject;
interface IRepository<T> {
add(model: T): Promise<void>;
update(model: T): Promise<void>;
delete(model: T): Promise<void>;
}
interface IUserRepository extends IRepository<User> {
existsByUsername(username: string): Promise<boolean>;
}
export class UserRepository implements IUserRepository {
constructor(
private readonly schema: MongooseSchema<UserSchema>,
private readonly factory: PersistenceObjectFactory<User>,
private readonly logger: Logger,
) { }
async add(user: User): Promise<void> {
const userModel = this.factory(user);
try {
await userModel.save();
} catch (error) {
this.addErrorHandler(error);
}
}
async update(user: User): Promise<void> {
const updateModel = {
destiny_id: user.destinyCharacter.accountId,
destiny_character_id: user.destinyCharacter.id,
refresh_expiration: user.bungieAccount.refreshToken.expiryDate.toISOString(),
refresh_token: user.bungieAccount.refreshToken.rawToken,
};
try {
await this.schema.updateOne(
{ bungie_membership_id: user.bungieAccount.id },
{ $set: updateModel },
this.updateErrorHandler.bind(this),
)
} catch (error) {
throw error;
}
}
async delete(user: User) {
// TODO: Implement
}
async existsByUsername(username: string): Promise<boolean> {
return await this.schema.exists({ bungie_username: username }).exec() ? true : false;
}
private addErrorHandler(error: Error): void {
this.logger.debug('Adding user failed');
}
private updateErrorHandler(error: Error): void {
if (error) {
this.logger.debug('Updating user record failed');
throw error;
} else {
this.logger.debug('Updated user record');
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment