Skip to content

fdorantesm/ts-ddd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Typescript Domain-Driven Design


A set of utilities for Domain-Driven Design

Packages

Packages Link
@ts-ddd/common Go to package
@ts-ddd/entity Go to package
@ts-ddd/state-machine Go to package
@ts-ddd/value-object Go to package

Common

UUID generator

import { uuidGenerator } from '@ts-ddd/common';

const uuid = uuidGenerator() // -> b9ebc3ea-bfbd-4a86-8b7b-39995b47c631

Interfaces

Interface Properties
WithActive isActive
WithAudit createdBy, updatedBy and deletedBy as date
WithDomain domain
WithLocale locale
WithNullable isNullable
WithOwner ownerId
WithScope scope
WithStatus status
WithTimestamps createdAt and deletedAt
WithUser userId
WithUuid uuid
WithVersion version

Example

interface Post extends WithActive, WithUuid {
    title: string;
    body: string;
}

/*
  {
    uuid: string;
    title: string;
    body: string;
    isActive: boolean;
  }
*/

Entity

Use to create domain entities using an interface as contract.

export interface IBook {
  id: Identifier;
  name: string;
  year: number;
  publisher: string;
}
import { uuidGenerator } from '@ts-ddd/common';
import { Identifier, Entity } from '@ts-ddd/entity';
import { IBook } from '../interfaces/book.interface';

export class Book extends Entity<IBook> {
  public static createToPrimitives(payload: Omit<IBook, 'id'>) {
    return new Book({
      id: uuidGenerator(),
      ...payload
    })
  }
  
  public static createFromPrimitives(payload: IBook): Book {
    return new Book(payload)
  }
}

Getting an object from the data source

class BooksService {
  constructor(private readonly booksRepository: BooksRepository) {}

  public async findBookById(id: string): Promise<Book | undefined> {
    const book = await this.booksRepository.findOne({ id });

    if (book) {
        return Book.createFromPrimitives(book)
    }

    return undefined;
  } 
}

Creating a book to store it in the data source

class CreateBookUseCase {
  constructor(private readonly booksService: BooksService) {}

  public async execute(payload: Omit<IBook, 'id'>): Promise<Book | undefined> {
    const book = await this.booksRepository.create(
        Book.createToPrimitives(payload)
    );

    return book;
  } 
}

State Machine

import { StateMachine } from '@ts-ddd/state-machine';

enum LightTransitions {
  RED = "red",
  GREEN = "green",
  PULSING_GREEN = "pulsing_green",
  AMBER = "amber",
}

const transitions = {
  [LightTransitions.RED]: [LightTransitions.GREEN],
  [LightTransitions.GREEN]: [LightTransitions.PULSING_GREEN],
  [LightTransitions.PULSING_GREEN]: [LightTransitions.AMBER],
  [LightTransitions.AMBER]: [LightTransitions.RED],
}

const light = StateMachine.instance({
  transitions,
  currentState: LightTransitions.RED,
});

try {
  light.setState(LightTransitions.GREEN);
} catch (error) {
  // InvalidStateTransitionException
}

Value Object

import { StringValue } from '@ts-ddd/value-object'
import { validate, v4 as uuid } from 'uuid';

export class Uuid extends StringValue {
  constructor(value: string) {
    super(value);
    this.ensureIsValidUuid(value);
  }

  static generate(): Uuid {
    return new Uuid(uuid());
  }

  private ensureIsValidUuid(value: string): void {
    if (!validate(value)) {
      throw new Error(`${value} is not a valid UUID`)
    }
  }
}
import { StringValue } from '@ts-ddd/value-object'

export class Title extends StringValue {}
import { PositiveNumberValue } from '@ts-ddd/value-object'

export class Year extends PositiveNumberValue {}
class BooksRepository {
  public async getBook(id: string): Promise<Book | undefined> {
    const book = await this.booksRepository.findOne({ id });

    if (!book) {
      return undefined;
    }

    return Book.createFromPrimitives({
      id: new Uuid(book.id),
      title: new Title(book.title),
      year: new Year(book.year)
    })
  }
}