├── README.md ├── after ├── README.md ├── jest.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── src │ ├── app.ts │ ├── index.ts │ ├── modules │ │ └── users │ │ │ ├── domain │ │ │ ├── email.ts │ │ │ ├── firstName.ts │ │ │ ├── lastName.ts │ │ │ ├── password.ts │ │ │ └── user.ts │ │ │ ├── repos │ │ │ ├── firebaseUserRepo.ts │ │ │ ├── index.ts │ │ │ └── userRepo.ts │ │ │ ├── services │ │ │ └── usersService.ts │ │ │ ├── testObjects │ │ │ └── userRepoSpy.ts │ │ │ └── useCases │ │ │ └── createUser │ │ │ ├── createUser.feature │ │ │ ├── createUser.spec.ts │ │ │ ├── createUser.ts │ │ │ ├── createUserController.ts │ │ │ └── index.ts │ └── shared │ │ ├── core │ │ ├── guard.ts │ │ ├── result.ts │ │ └── useCase.ts │ │ ├── domain │ │ ├── entity.ts │ │ ├── identifier.ts │ │ ├── uniqueEntityID.ts │ │ └── valueObject.ts │ │ └── types │ │ └── index.ts └── tsconfig.json └── before ├── README.md ├── jest.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── src ├── app.ts ├── index.ts ├── modules │ └── users │ │ ├── domain │ │ └── user.ts │ │ ├── repos │ │ ├── firebaseUserRepo.ts │ │ └── index.ts │ │ ├── services │ │ └── usersService.ts │ │ └── useCases │ │ └── createUser │ │ └── index.ts └── shared │ ├── core │ ├── guard.ts │ ├── result.ts │ └── useCase.ts │ ├── domain │ ├── entity.ts │ ├── identifier.ts │ ├── uniqueEntityID.ts │ └── valueObject.ts │ └── types │ └── index.ts └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # how-to-test-code-reliant-on-apis 2 | 3 | > The TDD process is simple enough, right? Red-green-refactor. Well, what about real-world scenarios? 4 | > This is the code for a (painfully recorded) video where we talk about how to use TDD to test code reliant on external APIs, services, databases, caches, webhooks, and so on. 5 | 6 | You can [watch the video here](https://youtu.be/ajfZqzeHp1E). 7 | -------------------------------------------------------------------------------- /after/README.md: -------------------------------------------------------------------------------- 1 | # 🧰 Simple TypeScript Starter | 2021 2 | 3 | > We talk about a lot of **advanced Node.js and TypeScript** concepts on [the blog](https://khalilstemmler.com), particularly focused around Domain-Driven Design and large-scale enterprise application patterns. However, I received a few emails from readers that were interested in seeing what a basic TypeScript starter project looks like. So I've put together just that. 4 | 5 | ### Features 6 | 7 | - Minimal 8 | - TypeScript v4 9 | - Testing with Jest 10 | - Linting with Eslint and Prettier 11 | - Pre-commit hooks with Husky 12 | - VS Code debugger scripts 13 | - Local development with Nodemon 14 | 15 | ### Scripts 16 | 17 | #### `npm run start:dev` 18 | 19 | Starts the application in development using `nodemon` and `ts-node` to do hot reloading. 20 | 21 | #### `npm run start` 22 | 23 | Starts the app in production by first building the project with `npm run build`, and then executing the compiled JavaScript at `build/index.js`. 24 | 25 | #### `npm run build` 26 | 27 | Builds the app at `build`, cleaning the folder first. 28 | 29 | #### `npm run test` 30 | 31 | Runs the `jest` tests once. 32 | 33 | #### `npm run test:dev` 34 | 35 | Run the `jest` tests in watch mode, waiting for file changes. 36 | 37 | #### `npm run prettier-format` 38 | 39 | Format your code. 40 | 41 | #### `npm run prettier-watch` 42 | 43 | Format your code in watch mode, waiting for file changes. 44 | -------------------------------------------------------------------------------- /after/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.ts?$': 'ts-jest' 4 | }, 5 | testEnvironment: 'node', 6 | testRegex: './src/.*\\.(test|spec)?\\.(ts|ts)$', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 8 | "roots": [ 9 | "/src" 10 | ] 11 | }; -------------------------------------------------------------------------------- /after/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": ".ts,.js", 4 | "ignore": [], 5 | "exec": "ts-node ./src/index.ts" 6 | } -------------------------------------------------------------------------------- /after/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-starter", 3 | "version": "1.0.0", 4 | "description": "A basic typescript app starter for newbies in 2019.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rimraf ./build && tsc", 8 | "start:dev": "nodemon", 9 | "start": "npm run build && node build/index.js", 10 | "lint": "eslint . --ext .ts", 11 | "prettier-format": "run-script-os", 12 | "prettier-format:win32": "prettier --config .prettierrc \"./src/**/*.ts\" --write", 13 | "prettier-format:darwin:linux": "prettier --config .prettierrc 'src/**/*.ts' --write", 14 | "prettier-format:default": "prettier --config .prettierrc 'src/**/*.ts' --write", 15 | "prettier-watch": "run-script-os", 16 | "prettier-watch:win32": "onchange \"src/**/*.ts\" -- prettier --write {{changed}}", 17 | "prettier-watch:darwin:linux": "onchange 'src/**/*.ts' -- prettier --write {{changed}}", 18 | "prettier-watch:default": "onchange 'src/**/*.ts' -- prettier --write {{changed}}", 19 | "test": "jest", 20 | "test:dev": "jest --watchAll" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "npm run test && npm run prettier-format && npm run lint" 25 | } 26 | }, 27 | "keywords": [], 28 | "author": "", 29 | "license": "ISC", 30 | "devDependencies": { 31 | "@types/jest": "^26.0.14", 32 | "@types/node": "^12.7.2", 33 | "@typescript-eslint/eslint-plugin": "^2.21.0", 34 | "@typescript-eslint/parser": "^2.21.0", 35 | "eslint": "^6.8.0", 36 | "eslint-config-prettier": "^6.10.0", 37 | "eslint-plugin-prettier": "^3.1.2", 38 | "husky": "^4.2.3", 39 | "jest-cucumber": "^3.0.1", 40 | "nodemon": "^1.19.1", 41 | "onchange": "^6.1.0", 42 | "prettier": "^1.19.1", 43 | "rimraf": "^3.0.0", 44 | "run-script-os": "^1.1.1", 45 | "ts-node": "^8.3.0", 46 | "typescript": "^4.0.3" 47 | }, 48 | "dependencies": { 49 | "@types/express": "^4.17.13", 50 | "eslint-plugin-jest": "^24.1.0", 51 | "express": "^4.17.1", 52 | "jest": "^26.5.3", 53 | "ts-jest": "^26.4.1", 54 | "uuid": "^8.3.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /after/src/app.ts: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express' 3 | import { Server } from 'http'; 4 | import { createUserController } from './modules/users/useCases/createUser'; 5 | 6 | const app = express() 7 | const port = 3000; 8 | 9 | let server: Server; 10 | 11 | app.get('/', (req, res) => createUserController.execute(req, res)) 12 | 13 | function start () { 14 | server = app.listen(port, () => { 15 | console.log(`Example app listening at http://localhost:${port}`) 16 | }) 17 | } 18 | 19 | function stop () { 20 | server.close(); 21 | } 22 | 23 | export { 24 | start, 25 | stop 26 | } -------------------------------------------------------------------------------- /after/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { start } from "./app"; 3 | 4 | start(); -------------------------------------------------------------------------------- /after/src/modules/users/domain/email.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "../../../shared/core/result" 2 | import { ValueObject } from "../../../shared/domain/valueObject" 3 | 4 | interface EmailProps { 5 | value: string; 6 | } 7 | 8 | export class Email extends ValueObject { 9 | 10 | getValue () { 11 | return this.props.value; 12 | } 13 | 14 | private constructor (props: EmailProps) { 15 | super(props); 16 | } 17 | 18 | public static isValidEmail (email: string) { 19 | const re = /\S+@\S+\.\S+/; 20 | return re.test(email); 21 | } 22 | 23 | public static create (email: string): Result { 24 | if (!this.isValidEmail(email)) { 25 | return Result.fail('Invalid email') 26 | } 27 | 28 | return Result.ok(new Email({ value: email })) 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /after/src/modules/users/domain/firstName.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Result } from "../../../shared/core/result" 3 | import { ValueObject } from "../../../shared/domain/valueObject" 4 | 5 | interface FirstNameProps { 6 | value: string; 7 | } 8 | 9 | export class FirstName extends ValueObject { 10 | 11 | private constructor (props: FirstNameProps) { 12 | super(props); 13 | } 14 | 15 | public static isValidFirstName (firstName: string) { 16 | return firstName.length >= 2; 17 | } 18 | 19 | public static create (firstName: string): Result { 20 | if (!this.isValidFirstName(firstName)) { 21 | return Result.fail('Invalid FirstName') 22 | } 23 | 24 | return Result.ok(new FirstName({ value: firstName })) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /after/src/modules/users/domain/lastName.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Result } from "../../../shared/core/result" 3 | import { ValueObject } from "../../../shared/domain/valueObject" 4 | 5 | interface LastNameProps { 6 | value: string; 7 | } 8 | 9 | export class LastName extends ValueObject { 10 | 11 | private constructor (props: LastNameProps) { 12 | super(props); 13 | } 14 | 15 | public static isValidLastName (lastName: string) { 16 | return lastName.length >= 2; 17 | } 18 | 19 | public static create (lastName: string): Result { 20 | if (!this.isValidLastName(lastName)) { 21 | return Result.fail('Invalid LastName') 22 | } 23 | 24 | return Result.ok(new LastName({ value: lastName })) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /after/src/modules/users/domain/password.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Result } from "../../../shared/core/result" 3 | import { ValueObject } from "../../../shared/domain/valueObject" 4 | 5 | interface PasswordProps { 6 | value: string; 7 | } 8 | 9 | export class Password extends ValueObject { 10 | 11 | private constructor (props: PasswordProps) { 12 | super(props); 13 | } 14 | 15 | public static isValidPassword (password: string) { 16 | return password.length >= 2; 17 | } 18 | 19 | public static create (password: string): Result { 20 | if (!this.isValidPassword(password)) { 21 | return Result.fail('Invalid Password') 22 | } 23 | 24 | return Result.ok(new Password({ value: password })) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /after/src/modules/users/domain/user.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Guard } from "../../../shared/core/guard"; 3 | import { Result } from "../../../shared/core/result"; 4 | import { Entity } from "../../../shared/domain/entity"; 5 | import { Email } from "./email"; 6 | import { FirstName } from "./firstName"; 7 | import { LastName } from "./lastName"; 8 | import { Password } from "./password"; 9 | 10 | interface UserProps { 11 | email: Email; 12 | firstName: FirstName; 13 | lastName: LastName; 14 | password: Password; 15 | } 16 | 17 | export class User extends Entity { 18 | 19 | getEmail (): Email { 20 | return this.props.email; 21 | } 22 | 23 | private constructor (props: UserProps) { 24 | super(props); 25 | } 26 | 27 | public static create (userProps: UserProps): Result { 28 | const guardResult = Guard.againstNullOrUndefinedBulk([ 29 | { argument: userProps.email, argumentName: 'email' }, 30 | { argument: userProps.firstName, argumentName: 'firstName' }, 31 | { argument: userProps.lastName, argumentName: 'lastName' }, 32 | { argument: userProps.password, argumentName: 'password' }, 33 | ]); 34 | 35 | if (!guardResult.succeeded) { 36 | return Result.fail(guardResult.message as string) 37 | } 38 | 39 | return Result.ok(new User(userProps)); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /after/src/modules/users/repos/firebaseUserRepo.ts: -------------------------------------------------------------------------------- 1 | import { Nothing } from "../../../shared/types"; 2 | import { User } from "../domain/user"; 3 | import { IUserRepo } from "./userRepo"; 4 | 5 | export class FirebaseUserRepo implements IUserRepo { 6 | 7 | constructor () { 8 | // Here's where I'd set up my firebase instance 9 | } 10 | 11 | async findByEmail (email: string): Promise { 12 | // And I'd use the firebase api to find the user by email 13 | 14 | return ''; 15 | } 16 | 17 | async save (user: User): Promise { 18 | // And I'd save the user to firebase in this method. 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /after/src/modules/users/repos/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { FirebaseUserRepo } from "./firebaseUserRepo"; 3 | 4 | const firebaseUserRepo = new FirebaseUserRepo(); 5 | 6 | export { 7 | firebaseUserRepo 8 | } -------------------------------------------------------------------------------- /after/src/modules/users/repos/userRepo.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Nothing } from "../../../shared/types"; 3 | import { User } from "../domain/user"; 4 | 5 | export interface IUserRepo { 6 | findByEmail (email: string): Promise; 7 | save (user: User): Promise; 8 | } -------------------------------------------------------------------------------- /after/src/modules/users/services/usersService.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export class UsersService { 4 | 5 | public static validateEmail (email: string) { 6 | const re = /\S+@\S+\.\S+/; 7 | return re.test(email); 8 | } 9 | 10 | public static validatePassword (password: string) { 11 | return password.length >= 6; 12 | } 13 | 14 | public static validateFirstName (firstName: string) { 15 | return firstName.length >= 2; 16 | } 17 | 18 | public static validateLastName (lastName: string) { 19 | return lastName.length >= 2; 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /after/src/modules/users/testObjects/userRepoSpy.ts: -------------------------------------------------------------------------------- 1 | import { Nothing } from "../../../shared/types"; 2 | import { User } from "../domain/user"; 3 | import { IUserRepo } from "../repos/userRepo"; 4 | 5 | export class UserRepoSpy implements IUserRepo { 6 | 7 | private users: User[]; 8 | private timesSaveCalled: number; 9 | 10 | constructor (users: User[]) { 11 | this.users = users; 12 | this.timesSaveCalled = 0; 13 | } 14 | 15 | async findByEmail (email: string): Promise { 16 | const found = this.users.find((u) => u.getEmail().getValue() === email); 17 | 18 | if (!found) { 19 | return '' 20 | } 21 | 22 | return found; 23 | } 24 | 25 | async save (user: User): Promise { 26 | this.timesSaveCalled++; 27 | } 28 | 29 | getTimesSaveCalled (): number { 30 | return this.timesSaveCalled; 31 | } 32 | } -------------------------------------------------------------------------------- /after/src/modules/users/useCases/createUser/createUser.feature: -------------------------------------------------------------------------------- 1 | Feature: Create user 2 | 3 | Scenario: Creating a user 4 | Given I provide valid user details 5 | When I attempt to create a user 6 | Then the user should be saved successfully 7 | 8 | Scenario: Invalid password 9 | Given I provide an invalid password 10 | When I attempt to create a user 11 | Then I should get an invalid details error -------------------------------------------------------------------------------- /after/src/modules/users/useCases/createUser/createUser.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { defineFeature, loadFeature } from 'jest-cucumber'; 3 | import * as path from 'path'; 4 | import { IUserRepo } from '../../repos/userRepo'; 5 | import { CreateUser, CreateUserResult } from './createUser' 6 | import { UserRepoSpy } from '../../testObjects/userRepoSpy' 7 | 8 | const feature = loadFeature(path.join(__dirname, './createUser.feature')); 9 | 10 | defineFeature(feature, test => { 11 | let result: CreateUserResult; 12 | 13 | let email: string; 14 | let password: string; 15 | let firstName: string; 16 | let lastName: string; 17 | 18 | let createUser: CreateUser; 19 | let userRepoSpy: UserRepoSpy; 20 | 21 | beforeEach(() => { 22 | createUser = undefined; 23 | userRepoSpy = undefined; 24 | }) 25 | 26 | test('Creating a user', ({ given, when, then }) => { 27 | 28 | given('I provide valid user details', () => { 29 | // Arrange 30 | email = 'khalil@khalilstemmler.com'; 31 | password = 'hello' 32 | firstName = 'khalil' 33 | lastName = 'stemmler'; 34 | 35 | userRepoSpy = new UserRepoSpy([]); 36 | 37 | createUser = new CreateUser(userRepoSpy); 38 | }); 39 | 40 | when('I attempt to create a user', async () => { 41 | // Act 42 | result = await createUser.execute({ email, password, firstName, lastName }); 43 | }); 44 | 45 | then('the user should be saved successfully', () => { 46 | // Assert 47 | expect(result.type).toEqual('CreateUserSuccess'); 48 | expect(userRepoSpy.getTimesSaveCalled()).toEqual(1); 49 | }); 50 | 51 | }); 52 | 53 | test('Invalid password', ({ given, when, then }) => { 54 | given('I provide an invalid password', () => { 55 | email = 'khalil@khalilstemmler.com'; 56 | password = '' 57 | firstName = 'khalil' 58 | lastName = 'stemmler'; 59 | 60 | userRepoSpy = new UserRepoSpy([]); 61 | 62 | createUser = new CreateUser(userRepoSpy); 63 | }); 64 | 65 | when('I attempt to create a user', async () => { 66 | result = await createUser.execute({ email, password, firstName, lastName }); 67 | }); 68 | 69 | then('I should get an invalid details error', () => { 70 | // Assert 71 | expect(result.type).toEqual('InvalidUserDetailsError') 72 | expect(userRepoSpy.getTimesSaveCalled()).toEqual(0); 73 | }); 74 | }); 75 | }); -------------------------------------------------------------------------------- /after/src/modules/users/useCases/createUser/createUser.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Result } from '../../../../shared/core/result'; 3 | import { UseCase } from '../../../../shared/core/useCase'; 4 | import { Email } from '../../domain/email'; 5 | import { FirstName } from '../../domain/firstName'; 6 | import { LastName } from '../../domain/lastName'; 7 | import { Password } from '../../domain/password'; 8 | import { User } from '../../domain/user'; 9 | import { IUserRepo } from '../../repos/userRepo'; 10 | 11 | type CreateUserInput = { 12 | email: string; 13 | password: string; 14 | firstName: string; 15 | lastName: string; 16 | } 17 | 18 | type CreateUserSuccess = { 19 | type: 'CreateUserSuccess' 20 | } 21 | 22 | type AlreadyRegisteredError = { 23 | type: 'AlreadyRegisteredError'; 24 | } 25 | 26 | type InvalidUserDetailsError = { 27 | type: 'InvalidUserDetailsError'; 28 | message: string; 29 | } 30 | 31 | type UnexpectedError = { 32 | type: 'UnexpectedError' 33 | } 34 | 35 | export type CreateUserResult = CreateUserSuccess 36 | | AlreadyRegisteredError 37 | | InvalidUserDetailsError 38 | | UnexpectedError; 39 | 40 | export class CreateUser implements UseCase { 41 | private userRepo: IUserRepo; 42 | 43 | constructor (userRepo: IUserRepo) { 44 | this.userRepo = userRepo; 45 | } 46 | 47 | public async execute (input: CreateUserInput): Promise { 48 | 49 | // Check to see if already registered 50 | const existingUser = await this.userRepo.findByEmail(input.email); 51 | 52 | // If already registered, return AlreadyRegisteredError 53 | if (existingUser) { 54 | return { 55 | type: 'AlreadyRegisteredError' 56 | } 57 | } 58 | 59 | // Validation logic 60 | let emailOrError = Email.create(input.email); 61 | let firstNameOrError = FirstName.create(input.firstName); 62 | let lastNameOrError = LastName.create(input.lastName); 63 | let passwordOrError = Password.create(input.password); 64 | 65 | let combinedResult = Result.combine([ 66 | emailOrError, firstNameOrError, lastNameOrError, passwordOrError 67 | ]); 68 | 69 | if (combinedResult.isFailure) { 70 | return { 71 | type: 'InvalidUserDetailsError', 72 | message: combinedResult.errorValue() 73 | } 74 | } 75 | 76 | let userOrError = User.create({ 77 | email: emailOrError.getValue() as Email, 78 | password: passwordOrError.getValue() as Password, 79 | firstName: firstNameOrError.getValue() as FirstName, 80 | lastName: lastNameOrError.getValue() as LastName 81 | }); 82 | 83 | if (userOrError.isFailure) { 84 | return { 85 | type: 'InvalidUserDetailsError', 86 | message: userOrError.errorValue() 87 | } 88 | } 89 | 90 | let user = userOrError.getValue() as User; 91 | 92 | // Save user to database 93 | try { 94 | await this.userRepo.save(user); 95 | } catch (err) { 96 | 97 | // Log this to monitoring or logging plugin but don't return 98 | // the backend error to the client. 99 | 100 | return { 101 | type: 'UnexpectedError' 102 | } 103 | } 104 | 105 | return { 106 | type: 'CreateUserSuccess' 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /after/src/modules/users/useCases/createUser/createUserController.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as express from 'express' 3 | import { CreateUser } from './createUser'; 4 | 5 | export class CreateUserController { 6 | 7 | private useCase: CreateUser; 8 | 9 | constructor (useCase: CreateUser) { 10 | this.useCase = useCase; 11 | } 12 | 13 | public async execute (req: express.Request, res: express.Response) { 14 | let body = req.body; 15 | 16 | // Check to see if firstname, lastname, password, email is in the request 17 | const isFirstNamePresent = body.firstName 18 | const isLastNamePresent = body.lastName; 19 | const isEmailPresent = body.email; 20 | const isPasswordPresent = body.password; 21 | 22 | // If not, end the request 23 | if (!isFirstNamePresent || !isEmailPresent || !isLastNamePresent || !isPasswordPresent) { 24 | return res.status(400).json({ 25 | message: `Either 'firstName', 'lastName', 'email' or 'password not present` 26 | }) 27 | } 28 | 29 | let email: string = body.email; 30 | let password: string = body.password; 31 | let firstName: string = body.firstName; 32 | let lastName: string = body.lastName; 33 | 34 | try { 35 | const result = await this.useCase.execute({ 36 | email, password, firstName, lastName 37 | }); 38 | 39 | switch (result.type) { 40 | case 'CreateUserSuccess': 41 | return res.status(201).json(result) 42 | case 'AlreadyRegisteredError': 43 | return res.status(409).json(result) 44 | case 'InvalidUserDetailsError': 45 | return res.status(400).json(result) 46 | case 'UnexpectedError': 47 | return res.status(500).json(result) 48 | } 49 | } catch (err) { 50 | // Report the error to metrics + logging app 51 | 52 | return res.status(500); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /after/src/modules/users/useCases/createUser/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { FirebaseUserRepo } from "../../repos/firebaseUserRepo"; 3 | import { CreateUser } from "./createUser"; 4 | import { CreateUserController } from "./createUserController"; 5 | 6 | const firebaseUserRepo = new FirebaseUserRepo(); 7 | 8 | const createUser = new CreateUser(firebaseUserRepo) 9 | 10 | const createUserController = new CreateUserController(createUser); 11 | 12 | export { 13 | createUserController 14 | } -------------------------------------------------------------------------------- /after/src/shared/core/guard.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface IGuardResult { 4 | succeeded: boolean; 5 | message?: string; 6 | } 7 | 8 | export interface IGuardArgument { 9 | argument: any; 10 | argumentName: string; 11 | } 12 | 13 | export type GuardArgumentCollection = IGuardArgument[]; 14 | 15 | export class Guard { 16 | public static combine (guardResults: IGuardResult[]): IGuardResult { 17 | for (let result of guardResults) { 18 | if (result.succeeded === false) return result; 19 | } 20 | 21 | return { succeeded: true }; 22 | } 23 | 24 | public static greaterThan (minValue: number, actualValue: number): IGuardResult { 25 | return actualValue > minValue 26 | ? { succeeded: true } 27 | : { 28 | succeeded: false, 29 | message: `Number given {${actualValue}} is not greater than {${minValue}}` 30 | } 31 | } 32 | 33 | public static againstAtLeast (numChars: number, text: string): IGuardResult { 34 | return text.length >= numChars 35 | ? { succeeded: true } 36 | : { 37 | succeeded: false, 38 | message: `Text is not at least ${numChars} chars.` 39 | } 40 | } 41 | 42 | public static againstAtMost (numChars: number, text: string): IGuardResult { 43 | return text.length <= numChars 44 | ? { succeeded: true } 45 | : { 46 | succeeded: false, 47 | message: `Text is greater than ${numChars} chars.` 48 | } 49 | } 50 | 51 | public static againstNullOrUndefined (argument: any, argumentName: string): IGuardResult { 52 | if (argument === null || argument === undefined) { 53 | return { succeeded: false, message: `${argumentName} is null or undefined` } 54 | } else { 55 | return { succeeded: true } 56 | } 57 | } 58 | 59 | public static againstNullOrUndefinedBulk(args: GuardArgumentCollection): IGuardResult { 60 | for (let arg of args) { 61 | const result = this.againstNullOrUndefined(arg.argument, arg.argumentName); 62 | if (!result.succeeded) return result; 63 | } 64 | 65 | return { succeeded: true } 66 | } 67 | 68 | public static isOneOf (value: any, validValues: any[], argumentName: string) : IGuardResult { 69 | let isValid = false; 70 | for (let validValue of validValues) { 71 | if (value === validValue) { 72 | isValid = true; 73 | } 74 | } 75 | 76 | if (isValid) { 77 | return { succeeded: true } 78 | } else { 79 | return { 80 | succeeded: false, 81 | message: `${argumentName} isn't oneOf the correct types in ${JSON.stringify(validValues)}. Got "${value}".` 82 | } 83 | } 84 | } 85 | 86 | public static inRange (num: number, min: number, max: number, argumentName: string) : IGuardResult { 87 | const isInRange = num >= min && num <= max; 88 | if (!isInRange) { 89 | return { succeeded: false, message: `${argumentName} is not within range ${min} to ${max}.`} 90 | } else { 91 | return { succeeded: true } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /after/src/shared/core/result.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Result { 3 | public isSuccess: boolean; 4 | public isFailure: boolean 5 | public error?: string; 6 | private _value?: T; 7 | 8 | public constructor (isSuccess: boolean, error?: string, value?: T) { 9 | if (isSuccess && error) { 10 | throw new Error("InvalidOperation: A result cannot be successful and contain an error"); 11 | } 12 | if (!isSuccess && !error) { 13 | throw new Error("InvalidOperation: A failing result needs to contain an error message"); 14 | } 15 | 16 | this.isSuccess = isSuccess; 17 | this.isFailure = !isSuccess; 18 | this.error = error; 19 | this._value = value; 20 | 21 | Object.freeze(this); 22 | } 23 | 24 | public getValue () : T | undefined { 25 | if (!this.isSuccess) { 26 | console.log(this.error,); 27 | throw new Error("Can't get the value of an error result. Use 'errorValue' instead.") 28 | } 29 | 30 | return this._value; 31 | } 32 | 33 | public errorValue (): string { 34 | return this.error as string; 35 | } 36 | 37 | public static ok (value?: U) : Result { 38 | return new Result(true, undefined, value); 39 | } 40 | 41 | public static fail (error: string): Result { 42 | return new Result(false, error); 43 | } 44 | 45 | public static combine (results: Result[]) : Result { 46 | for (let result of results) { 47 | if (result.isFailure) return result; 48 | } 49 | return Result.ok(); 50 | } 51 | } 52 | 53 | export type Either = Left | Right; 54 | 55 | export class Left { 56 | readonly value: L; 57 | 58 | constructor(value: L) { 59 | this.value = value; 60 | } 61 | 62 | isLeft(): this is Left { 63 | return true; 64 | } 65 | 66 | isRight(): this is Right { 67 | return false; 68 | } 69 | } 70 | 71 | export class Right { 72 | readonly value: A; 73 | 74 | constructor(value: A) { 75 | this.value = value; 76 | } 77 | 78 | isLeft(): this is Left { 79 | return false; 80 | } 81 | 82 | isRight(): this is Right { 83 | return true; 84 | } 85 | } 86 | 87 | export const left = (l: L): Either => { 88 | return new Left(l); 89 | }; 90 | 91 | export const right = (a: A): Either => { 92 | return new Right(a); 93 | }; -------------------------------------------------------------------------------- /after/src/shared/core/useCase.ts: -------------------------------------------------------------------------------- 1 | export interface UseCase { 2 | execute (input?: In): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /after/src/shared/domain/entity.ts: -------------------------------------------------------------------------------- 1 | import { UniqueEntityID } from './UniqueEntityID'; 2 | 3 | const isEntity = (v: any): v is Entity => { 4 | return v instanceof Entity; 5 | }; 6 | 7 | export abstract class Entity { 8 | protected readonly _id: UniqueEntityID; 9 | public readonly props: T; 10 | 11 | constructor (props: T, id?: UniqueEntityID) { 12 | this._id = id ? id : new UniqueEntityID(); 13 | this.props = props; 14 | } 15 | 16 | public equals (object?: Entity) : boolean { 17 | 18 | if (object == null || object == undefined) { 19 | return false; 20 | } 21 | 22 | if (this === object) { 23 | return true; 24 | } 25 | 26 | if (!isEntity(object)) { 27 | return false; 28 | } 29 | 30 | return this._id.equals(object._id); 31 | } 32 | } -------------------------------------------------------------------------------- /after/src/shared/domain/identifier.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Identifier { 3 | constructor(private value: T) { 4 | this.value = value; 5 | } 6 | 7 | equals (id?: Identifier): boolean { 8 | if (id === null || id === undefined) { 9 | return false; 10 | } 11 | if (!(id instanceof this.constructor)) { 12 | return false; 13 | } 14 | return id.toValue() === this.value; 15 | } 16 | 17 | toString () { 18 | return String(this.value); 19 | } 20 | 21 | /** 22 | * Return raw value of identifier 23 | */ 24 | 25 | toValue (): T { 26 | return this.value; 27 | } 28 | } -------------------------------------------------------------------------------- /after/src/shared/domain/uniqueEntityID.ts: -------------------------------------------------------------------------------- 1 | 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { Identifier } from './identifier' 4 | 5 | export class UniqueEntityID extends Identifier{ 6 | constructor (id?: string | number) { 7 | super(id ? id : uuidv4()) 8 | } 9 | } -------------------------------------------------------------------------------- /after/src/shared/domain/valueObject.ts: -------------------------------------------------------------------------------- 1 | interface ValueObjectProps { 2 | [index: string]: any; 3 | } 4 | 5 | /** 6 | * @desc ValueObjects are objects that we determine their 7 | * equality through their structrual property. 8 | */ 9 | 10 | export abstract class ValueObject { 11 | public props: T; 12 | 13 | constructor (props: T) { 14 | let baseProps: any = { 15 | ...props, 16 | } 17 | 18 | this.props = baseProps; 19 | } 20 | 21 | public equals (vo?: ValueObject) : boolean { 22 | if (vo === null || vo === undefined) { 23 | return false; 24 | } 25 | if (vo.props === undefined) { 26 | return false; 27 | } 28 | return JSON.stringify(this.props) === JSON.stringify(vo.props); 29 | } 30 | } -------------------------------------------------------------------------------- /after/src/shared/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Nothing = '' -------------------------------------------------------------------------------- /after/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "allowJs": true, 7 | "outDir": "build", 8 | "rootDir": "src", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "types": ["node", "@types/jest"], 14 | "typeRoots" : ["./node_modules/@types"], 15 | }, 16 | "include": [ 17 | "src/**/*" 18 | ], 19 | "exclude" : [ 20 | "src/**/*.spec.ts" 21 | ] 22 | } -------------------------------------------------------------------------------- /before/README.md: -------------------------------------------------------------------------------- 1 | # 🧰 Simple TypeScript Starter | 2021 2 | 3 | > We talk about a lot of **advanced Node.js and TypeScript** concepts on [the blog](https://khalilstemmler.com), particularly focused around Domain-Driven Design and large-scale enterprise application patterns. However, I received a few emails from readers that were interested in seeing what a basic TypeScript starter project looks like. So I've put together just that. 4 | 5 | ### Features 6 | 7 | - Minimal 8 | - TypeScript v4 9 | - Testing with Jest 10 | - Linting with Eslint and Prettier 11 | - Pre-commit hooks with Husky 12 | - VS Code debugger scripts 13 | - Local development with Nodemon 14 | 15 | ### Scripts 16 | 17 | #### `npm run start:dev` 18 | 19 | Starts the application in development using `nodemon` and `ts-node` to do hot reloading. 20 | 21 | #### `npm run start` 22 | 23 | Starts the app in production by first building the project with `npm run build`, and then executing the compiled JavaScript at `build/index.js`. 24 | 25 | #### `npm run build` 26 | 27 | Builds the app at `build`, cleaning the folder first. 28 | 29 | #### `npm run test` 30 | 31 | Runs the `jest` tests once. 32 | 33 | #### `npm run test:dev` 34 | 35 | Run the `jest` tests in watch mode, waiting for file changes. 36 | 37 | #### `npm run prettier-format` 38 | 39 | Format your code. 40 | 41 | #### `npm run prettier-watch` 42 | 43 | Format your code in watch mode, waiting for file changes. 44 | -------------------------------------------------------------------------------- /before/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.ts?$': 'ts-jest' 4 | }, 5 | testEnvironment: 'node', 6 | testRegex: './src/.*\\.(test|spec)?\\.(ts|ts)$', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 8 | "roots": [ 9 | "/src" 10 | ] 11 | }; -------------------------------------------------------------------------------- /before/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": ".ts,.js", 4 | "ignore": [], 5 | "exec": "ts-node ./src/index.ts" 6 | } -------------------------------------------------------------------------------- /before/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-starter", 3 | "version": "1.0.0", 4 | "description": "A basic typescript app starter for newbies in 2019.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rimraf ./build && tsc", 8 | "start:dev": "nodemon", 9 | "start": "npm run build && node build/index.js", 10 | "lint": "eslint . --ext .ts", 11 | "prettier-format": "run-script-os", 12 | "prettier-format:win32": "prettier --config .prettierrc \"./src/**/*.ts\" --write", 13 | "prettier-format:darwin:linux": "prettier --config .prettierrc 'src/**/*.ts' --write", 14 | "prettier-format:default": "prettier --config .prettierrc 'src/**/*.ts' --write", 15 | "prettier-watch": "run-script-os", 16 | "prettier-watch:win32": "onchange \"src/**/*.ts\" -- prettier --write {{changed}}", 17 | "prettier-watch:darwin:linux": "onchange 'src/**/*.ts' -- prettier --write {{changed}}", 18 | "prettier-watch:default": "onchange 'src/**/*.ts' -- prettier --write {{changed}}", 19 | "test": "jest", 20 | "test:dev": "jest --watchAll" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "npm run test && npm run prettier-format && npm run lint" 25 | } 26 | }, 27 | "keywords": [], 28 | "author": "", 29 | "license": "ISC", 30 | "devDependencies": { 31 | "@types/jest": "^26.0.14", 32 | "@types/node": "^12.7.2", 33 | "@typescript-eslint/eslint-plugin": "^2.21.0", 34 | "@typescript-eslint/parser": "^2.21.0", 35 | "eslint": "^6.8.0", 36 | "eslint-config-prettier": "^6.10.0", 37 | "eslint-plugin-prettier": "^3.1.2", 38 | "husky": "^4.2.3", 39 | "jest-cucumber": "^3.0.1", 40 | "nodemon": "^1.19.1", 41 | "onchange": "^6.1.0", 42 | "prettier": "^1.19.1", 43 | "rimraf": "^3.0.0", 44 | "run-script-os": "^1.1.1", 45 | "ts-node": "^8.3.0", 46 | "typescript": "^4.0.3" 47 | }, 48 | "dependencies": { 49 | "@types/express": "^4.17.13", 50 | "eslint-plugin-jest": "^24.1.0", 51 | "express": "^4.17.1", 52 | "jest": "^26.5.3", 53 | "ts-jest": "^26.4.1", 54 | "uuid": "^8.3.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /before/src/app.ts: -------------------------------------------------------------------------------- 1 | 2 | import express from 'express' 3 | import { Server } from 'http'; 4 | import { createUser } from './modules/users/useCases/createUser' 5 | 6 | const app = express() 7 | const port = 3000; 8 | 9 | let server: Server; 10 | 11 | app.get('/', (req, res) => createUser(req, res)) 12 | 13 | function start () { 14 | server = app.listen(port, () => { 15 | console.log(`Example app listening at http://localhost:${port}`) 16 | }) 17 | } 18 | 19 | function stop () { 20 | server.close(); 21 | } 22 | 23 | export { 24 | start, 25 | stop 26 | } -------------------------------------------------------------------------------- /before/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { start } from "./app"; 3 | 4 | start(); -------------------------------------------------------------------------------- /before/src/modules/users/domain/user.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface User { 4 | email: string; 5 | firstName: string; 6 | lastName: string; 7 | password: string; 8 | } 9 | -------------------------------------------------------------------------------- /before/src/modules/users/repos/firebaseUserRepo.ts: -------------------------------------------------------------------------------- 1 | import { Nothing } from "../../../shared/types"; 2 | import { User } from "../domain/user"; 3 | 4 | export class FirebaseUserRepo { 5 | 6 | constructor () { 7 | // Here's where I'd set up my firebase instance 8 | } 9 | 10 | async findByEmail (email: string): Promise { 11 | // And I'd use the firebase api to find the user by email 12 | 13 | return ''; 14 | } 15 | 16 | async save (user: User): Promise { 17 | // And I'd save the user to firebase in this method. 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /before/src/modules/users/repos/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { FirebaseUserRepo } from "./firebaseUserRepo"; 3 | 4 | const firebaseUserRepo = new FirebaseUserRepo(); 5 | 6 | export { 7 | firebaseUserRepo 8 | } -------------------------------------------------------------------------------- /before/src/modules/users/services/usersService.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export class UsersService { 4 | 5 | public static validateEmail (email: string) { 6 | const re = /\S+@\S+\.\S+/; 7 | return re.test(email); 8 | } 9 | 10 | public static validatePassword (password: string) { 11 | return password.length >= 6; 12 | } 13 | 14 | public static validateFirstName (firstName: string) { 15 | return firstName.length >= 2; 16 | } 17 | 18 | public static validateLastName (lastName: string) { 19 | return lastName.length >= 2; 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /before/src/modules/users/useCases/createUser/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as express from 'express' 3 | import { User } from '../../domain/user'; 4 | import { firebaseUserRepo } from '../../repos'; 5 | import { UsersService } from '../../services/usersService' 6 | 7 | export async function createUser (req: express.Request, res: express.Response) { 8 | let body = req.body; 9 | 10 | // Check to see if firstname, lastname, password, email is in the request 11 | const isFirstNamePresent = body.firstName 12 | const isLastNamePresent = body.lastName; 13 | const isEmailPresent = body.email; 14 | const isPasswordPresent = body.password; 15 | 16 | // If not, end the request 17 | if (!isFirstNamePresent || !isEmailPresent || !isLastNamePresent || !isPasswordPresent) { 18 | return res.status(400).json({ 19 | message: `Either 'firstName', 'lastName', 'email' or 'password not present` 20 | }) 21 | } 22 | 23 | // Check to see if already registered 24 | const existingUser = await firebaseUserRepo.findByEmail(body.email); 25 | 26 | // If already registered, return AlreadyRegisteredError 27 | if (existingUser) { 28 | return res.status(409).json({ 29 | type: `AlreadyRegisteredError`, 30 | message: 'User already registered' 31 | }) 32 | } 33 | 34 | let errorMessage; 35 | 36 | // Validation logic 37 | if (UsersService.validateFirstName(body.firstName)) { 38 | errorMessage = 'Invalid firstName'; 39 | } 40 | 41 | if (UsersService.validateLastName(body.lastName)) { 42 | errorMessage = 'Invalid lastName'; 43 | } 44 | 45 | if (UsersService.validateEmail(body.email)) { 46 | errorMessage = 'Invalid email'; 47 | } 48 | 49 | if (UsersService.validatePassword(body.password)) { 50 | errorMessage = 'Invalid password'; 51 | } 52 | 53 | // If invalid props, return InvalidUserDetailsError 54 | if (errorMessage) { 55 | return res.status(400).json({ 56 | type: 'InvalidUserDetailsError', 57 | message: errorMessage 58 | }) 59 | } 60 | 61 | // Create user 62 | let user: User = { 63 | firstName: body.firstName, 64 | lastName: body.lastName, 65 | email: body.email, 66 | password: body.password 67 | } 68 | 69 | // Save user to database 70 | try { 71 | await firebaseUserRepo.save(user); 72 | } catch (err) { 73 | 74 | // Log this to monitoring or logging plugin but don't return 75 | // the backend error to the client. 76 | 77 | return res.status(500).json({ 78 | message: 'Unexpected error occurred' 79 | }) 80 | } 81 | 82 | return res.status(201).json({ 83 | type: 'CreateUserSuccess', 84 | message: 'Success' 85 | }) 86 | } 87 | 88 | -------------------------------------------------------------------------------- /before/src/shared/core/guard.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface IGuardResult { 4 | succeeded: boolean; 5 | message?: string; 6 | } 7 | 8 | export interface IGuardArgument { 9 | argument: any; 10 | argumentName: string; 11 | } 12 | 13 | export type GuardArgumentCollection = IGuardArgument[]; 14 | 15 | export class Guard { 16 | public static combine (guardResults: IGuardResult[]): IGuardResult { 17 | for (let result of guardResults) { 18 | if (result.succeeded === false) return result; 19 | } 20 | 21 | return { succeeded: true }; 22 | } 23 | 24 | public static greaterThan (minValue: number, actualValue: number): IGuardResult { 25 | return actualValue > minValue 26 | ? { succeeded: true } 27 | : { 28 | succeeded: false, 29 | message: `Number given {${actualValue}} is not greater than {${minValue}}` 30 | } 31 | } 32 | 33 | public static againstAtLeast (numChars: number, text: string): IGuardResult { 34 | return text.length >= numChars 35 | ? { succeeded: true } 36 | : { 37 | succeeded: false, 38 | message: `Text is not at least ${numChars} chars.` 39 | } 40 | } 41 | 42 | public static againstAtMost (numChars: number, text: string): IGuardResult { 43 | return text.length <= numChars 44 | ? { succeeded: true } 45 | : { 46 | succeeded: false, 47 | message: `Text is greater than ${numChars} chars.` 48 | } 49 | } 50 | 51 | public static againstNullOrUndefined (argument: any, argumentName: string): IGuardResult { 52 | if (argument === null || argument === undefined) { 53 | return { succeeded: false, message: `${argumentName} is null or undefined` } 54 | } else { 55 | return { succeeded: true } 56 | } 57 | } 58 | 59 | public static againstNullOrUndefinedBulk(args: GuardArgumentCollection): IGuardResult { 60 | for (let arg of args) { 61 | const result = this.againstNullOrUndefined(arg.argument, arg.argumentName); 62 | if (!result.succeeded) return result; 63 | } 64 | 65 | return { succeeded: true } 66 | } 67 | 68 | public static isOneOf (value: any, validValues: any[], argumentName: string) : IGuardResult { 69 | let isValid = false; 70 | for (let validValue of validValues) { 71 | if (value === validValue) { 72 | isValid = true; 73 | } 74 | } 75 | 76 | if (isValid) { 77 | return { succeeded: true } 78 | } else { 79 | return { 80 | succeeded: false, 81 | message: `${argumentName} isn't oneOf the correct types in ${JSON.stringify(validValues)}. Got "${value}".` 82 | } 83 | } 84 | } 85 | 86 | public static inRange (num: number, min: number, max: number, argumentName: string) : IGuardResult { 87 | const isInRange = num >= min && num <= max; 88 | if (!isInRange) { 89 | return { succeeded: false, message: `${argumentName} is not within range ${min} to ${max}.`} 90 | } else { 91 | return { succeeded: true } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /before/src/shared/core/result.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Result { 3 | public isSuccess: boolean; 4 | public isFailure: boolean 5 | public error?: string; 6 | private _value?: T; 7 | 8 | public constructor (isSuccess: boolean, error?: string, value?: T) { 9 | if (isSuccess && error) { 10 | throw new Error("InvalidOperation: A result cannot be successful and contain an error"); 11 | } 12 | if (!isSuccess && !error) { 13 | throw new Error("InvalidOperation: A failing result needs to contain an error message"); 14 | } 15 | 16 | this.isSuccess = isSuccess; 17 | this.isFailure = !isSuccess; 18 | this.error = error; 19 | this._value = value; 20 | 21 | Object.freeze(this); 22 | } 23 | 24 | public getValue () : T | undefined { 25 | if (!this.isSuccess) { 26 | console.log(this.error,); 27 | throw new Error("Can't get the value of an error result. Use 'errorValue' instead.") 28 | } 29 | 30 | return this._value; 31 | } 32 | 33 | public errorValue (): string { 34 | return this.error as string; 35 | } 36 | 37 | public static ok (value?: U) : Result { 38 | return new Result(true, undefined, value); 39 | } 40 | 41 | public static fail (error: string): Result { 42 | return new Result(false, error); 43 | } 44 | 45 | public static combine (results: Result[]) : Result { 46 | for (let result of results) { 47 | if (result.isFailure) return result; 48 | } 49 | return Result.ok(); 50 | } 51 | } 52 | 53 | export type Either = Left | Right; 54 | 55 | export class Left { 56 | readonly value: L; 57 | 58 | constructor(value: L) { 59 | this.value = value; 60 | } 61 | 62 | isLeft(): this is Left { 63 | return true; 64 | } 65 | 66 | isRight(): this is Right { 67 | return false; 68 | } 69 | } 70 | 71 | export class Right { 72 | readonly value: A; 73 | 74 | constructor(value: A) { 75 | this.value = value; 76 | } 77 | 78 | isLeft(): this is Left { 79 | return false; 80 | } 81 | 82 | isRight(): this is Right { 83 | return true; 84 | } 85 | } 86 | 87 | export const left = (l: L): Either => { 88 | return new Left(l); 89 | }; 90 | 91 | export const right = (a: A): Either => { 92 | return new Right(a); 93 | }; -------------------------------------------------------------------------------- /before/src/shared/core/useCase.ts: -------------------------------------------------------------------------------- 1 | export interface UseCase { 2 | execute (input?: In): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /before/src/shared/domain/entity.ts: -------------------------------------------------------------------------------- 1 | import { UniqueEntityID } from './UniqueEntityID'; 2 | 3 | const isEntity = (v: any): v is Entity => { 4 | return v instanceof Entity; 5 | }; 6 | 7 | export abstract class Entity { 8 | protected readonly _id: UniqueEntityID; 9 | public readonly props: T; 10 | 11 | constructor (props: T, id?: UniqueEntityID) { 12 | this._id = id ? id : new UniqueEntityID(); 13 | this.props = props; 14 | } 15 | 16 | public equals (object?: Entity) : boolean { 17 | 18 | if (object == null || object == undefined) { 19 | return false; 20 | } 21 | 22 | if (this === object) { 23 | return true; 24 | } 25 | 26 | if (!isEntity(object)) { 27 | return false; 28 | } 29 | 30 | return this._id.equals(object._id); 31 | } 32 | } -------------------------------------------------------------------------------- /before/src/shared/domain/identifier.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Identifier { 3 | constructor(private value: T) { 4 | this.value = value; 5 | } 6 | 7 | equals (id?: Identifier): boolean { 8 | if (id === null || id === undefined) { 9 | return false; 10 | } 11 | if (!(id instanceof this.constructor)) { 12 | return false; 13 | } 14 | return id.toValue() === this.value; 15 | } 16 | 17 | toString () { 18 | return String(this.value); 19 | } 20 | 21 | /** 22 | * Return raw value of identifier 23 | */ 24 | 25 | toValue (): T { 26 | return this.value; 27 | } 28 | } -------------------------------------------------------------------------------- /before/src/shared/domain/uniqueEntityID.ts: -------------------------------------------------------------------------------- 1 | 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { Identifier } from './identifier' 4 | 5 | export class UniqueEntityID extends Identifier{ 6 | constructor (id?: string | number) { 7 | super(id ? id : uuidv4()) 8 | } 9 | } -------------------------------------------------------------------------------- /before/src/shared/domain/valueObject.ts: -------------------------------------------------------------------------------- 1 | interface ValueObjectProps { 2 | [index: string]: any; 3 | } 4 | 5 | /** 6 | * @desc ValueObjects are objects that we determine their 7 | * equality through their structrual property. 8 | */ 9 | 10 | export abstract class ValueObject { 11 | public props: T; 12 | 13 | constructor (props: T) { 14 | let baseProps: any = { 15 | ...props, 16 | } 17 | 18 | this.props = baseProps; 19 | } 20 | 21 | public equals (vo?: ValueObject) : boolean { 22 | if (vo === null || vo === undefined) { 23 | return false; 24 | } 25 | if (vo.props === undefined) { 26 | return false; 27 | } 28 | return JSON.stringify(this.props) === JSON.stringify(vo.props); 29 | } 30 | } -------------------------------------------------------------------------------- /before/src/shared/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Nothing = '' -------------------------------------------------------------------------------- /before/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "allowJs": true, 7 | "outDir": "build", 8 | "rootDir": "src", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "types": ["node", "@types/jest"], 14 | "typeRoots" : ["./node_modules/@types"], 15 | }, 16 | "include": [ 17 | "src/**/*" 18 | ], 19 | "exclude" : [ 20 | "src/**/*.spec.ts" 21 | ] 22 | } --------------------------------------------------------------------------------