Last active
February 12, 2024 19:29
-
-
Save mkuchak/959ab4ee0de5e43f78289b32f8a1656c to your computer and use it in GitHub Desktop.
TypeScript alias alternative to Static Factory Method: avoid static create and restore methods on DDD, just simplify
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class Email { | |
constructor(private _value: string) { | |
if (!this.isValid()) { | |
throw new Error("Invalid email"); | |
} | |
} | |
private isValid(): boolean { | |
/** | |
* Must have a valid username and domain. | |
* Allows letters, digits, hyphens, and underscores in the username. | |
* Allows letters and hyphens in the domain. | |
* Must have a valid top-level domain (TLD). | |
* Forbids the use of alias with '+' in the username. | |
*/ | |
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; | |
return regex.test(this._value); | |
} | |
get value(): string { | |
return this._value.trim(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare global { | |
type ExcludeMethods<T> = Pick< | |
T, | |
{ | |
[K in keyof T]: T[K] extends Function ? never : K; | |
}[keyof T] | |
>; | |
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X | |
? 1 | |
: 2) extends <T>() => T extends Y ? 1 : 2 | |
? A | |
: B; | |
type WritableKeys<T> = { | |
[P in keyof T]-?: IfEquals< | |
{ [Q in P]: T[P] }, | |
{ -readonly [Q in P]: T[P] }, | |
P | |
>; | |
}[keyof T]; | |
type ExtractClassProps<T> = ExcludeMethods<Pick<T, WritableKeys<T>>>; | |
// type ClassProps<T, U = ExtractClassProps<T>> = Omit< | |
// ExtractClassProps<T>, | |
// keyof U | |
// > & | |
// U; | |
type ClassProps< | |
T, | |
U = ExtractClassProps<T>, | |
V extends keyof T = never | |
> = Omit<ExtractClassProps<T>, keyof U | V> & U; | |
} | |
export {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { User } from "./User"; | |
const main = async () => { | |
const newUser = new User({ // naturally replaces the static create method | |
email: "[email protected]", | |
password: "123456789", | |
surname: "Doe", | |
givenName: "John", | |
middleName: "Smith", | |
}); | |
console.log(newUser.id); // get the new generated uuid | |
await newUser.password.hash(); // hash the password | |
console.log(newUser.password.value); // get the hashed password as string | |
console.log(newUser.fullName); // `John Smith Doe` | |
const oldUser = new User({ // naturally replaces the static restore method | |
id: "bf54c5a8-3ec4-4c5c-9577-4ec638dbf00f", | |
email: "[email protected]", | |
password: "$argon2...", // hashed password | |
surname: "Doe", | |
givenName: "Jane", | |
}); | |
const oldUserPassword = "123456789"; | |
console.log(oldUser.id); // get the old uuid `bf54c5a8-3ec4-4c5c-9577-4ec638dbf00f` | |
console.log(oldUser.email.value); // get the email as string from Email value object `[email protected]` | |
console.log(await oldUser.password.verify(oldUserPassword)); // verify the password (returns a boolean promise) | |
}; | |
main(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import argon2 from "argon2"; | |
export class Password { | |
constructor(private _value: string) { | |
if (argon2.needsRehash(this._value) && !this.isValid()) { | |
throw new Error("Invalid password"); | |
} | |
} | |
private isValid(): boolean { | |
/** | |
* At least 8 characters. | |
* At least one lowercase letter. | |
* At least one uppercase letter. | |
* At least one number. | |
* At least one special character. | |
*/ | |
const regex = | |
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; | |
return regex.test(this._value); | |
} | |
async hash(): Promise<void> { | |
if (!argon2.needsRehash(this._value)) return; | |
this._value = await argon2.hash(this._value); | |
} | |
async verify(password: string): Promise<boolean> { | |
return await argon2.verify(this._value, password); | |
} | |
get value(): string { | |
return this._value; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import crypto from "node:crypto"; | |
import { Email } from "./Email"; | |
import { Password } from "./Password"; | |
export type UserProps = ClassProps< | |
User, | |
{ // this second type is optional and will replace the original User class props | |
email: string; | |
password: string; | |
} | |
>; | |
/** | |
* UserProps will be: | |
* { | |
* id?: string; | |
* email: string; | |
* password: string; | |
* surname: string; | |
* givenName: string; | |
* middleName?: string; | |
* } | |
*/ | |
export class User { // entity | |
id?: string = crypto.randomUUID(); | |
email: Email; | |
password: Password; | |
surname: string; | |
givenName: string; | |
middleName?: string; | |
constructor(props: UserProps) { // props allows you to use a single object instead of stacking multiple arguments in the constructor | |
Object.assign(this, props); | |
this.email = new Email(props.email); // value object | |
this.password = new Password(props.password); // value object | |
} | |
get fullName(): string { | |
return `${this.givenName} ${this.middleName ? `${this.middleName} ` : ""}${this.surname}`; | |
} | |
// more behavior... | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment