Skip to content
Franklin Chieze edited this page Mar 5, 2024 · 18 revisions

Datadom

This package is designed to help you define, tests, and execute your business domains, domain models, and domain rules.

Goals

The primary goal of this project is to create a technology-agnostic way of representing business domains (with their models, rules, and logic) so that projects can be built in a truly domain-driven manner. By doing so:

  • We can produce code that's easier to picture because we use explicit models, rules, and logic.
  • We can produce clean code with obvious layers of abstraction and reduced dependencies on tools.
  • We can produce domain-specific code that can be used anywhere (frontend, backend, mobile, etc.) because there is a very clear distinction between our domain ideas and the tools used in implementing those ideas.
  • We can easily define domain boundaries, which is useful whether you are working on a monolith or a microservice.
  • It becomes easier to separate side effects from domain rules and logic.

Installation

npm i @datadom/core

Domain

This represents the problem space your project occupies and provides a solution to.

import { Domain } from '@datadom/core';

const domain = new Domain();

Read more ➡️

Models

Domain models map to your business entities.

Datadom does not provide specific facilities for representing domain models. There is no interface to implement or class to inherit.

Read more ➡️

Repositories

A repository is an object that represents a collection of models. Typically, a repository would be responsible for CRUD (Create Read Update Delete) operations on models.

import { ID, IRepository } from '@datadom/core';

interface ICharacter {
  id?: ID;
  name: string;
  createdAt?: Date;
  updatedAt?: Date;
}

interface ICharacterRepository extends IRepository<ICharacter> {}

export class CharacterRepository implements ICharacterRepository {
    count(params?: IQueryBuilder<ICharacter>): Promise<number> {
        throw new Error('Method not implemented.');
    }
    delete(id: string): Promise<OperationResult> {
        throw new Error('Method not implemented.');
    }
    deleteMany(params: IQueryBuilder<ICharacter>): Promise<OperationResult> {
        throw new Error('Method not implemented.');
    }
    exists(params: IQueryBuilder<ICharacter>): Promise<boolean> {
        throw new Error('Method not implemented.');
    }
    get(id: string): Promise<ICharacter | null> {
        throw new Error('Method not implemented.');
    }
    getMany(params?: IQueryBuilder<ICharacter>): Promise<ICharacter[]> {
        throw new Error('Method not implemented.');
    }
    save(data: SaveInput<ICharacter>): Promise<ICharacter> {
        throw new Error('Method not implemented.');
    }
    update(id: string, data: UpdateInput<ICharacter>): Promise<OperationResult> {
        throw new Error('Method not implemented.');
    }
    updateMany(params: IQueryBuilder<ICharacter>, data: UpdateInput<ICharacter>): Promise<OperationResult> {
        throw new Error('Method not implemented.');
    }
}

Read more ➡️

Services

A service is a wrapper around a repository.

Whenever you register a repository in the domain by calling domain.registerRepository, a service of the same name is created under the hood. This service exposes the same methods exposed by the repository. However, a service ensures that the relevant rules, events, and middleware are run before and after certain repository actions.

import { Domain } from '@datadom/core';
import { CharacterRepository } from './characters';

const domain = new Domain();

domain.registerRepository('character', new CharacterRepository());

After registering a repository with a domain, you can access the wrapping service in a number of ways, depending on how strict your type-checking is.

// access the character service using any of the following notations:

domain.$('character');
(domain as any).$character;
(domain as any)['$character'];
domain.$character; // without strict type-checking
domain['$character']; // without strict type-checking

You can attach various hooks, rules, and event listeners to a service to perform actions before and after various repository operations. This helps the repository methods to concerned with only their tasks and not have to worry about preconditions, checks, and side effects.

domain.$('character').addRule('save', (data) => {
  console.log('This rule runs before an entity is saved by the repository');

  // ensure that the entity to be saved has a "name" field
  return !!(data.name);
});
domain.$('character').pre('save', (data, next) => {
  console.log('This hook/middleware runs before an entity is saved by the repository');

  // call "next" to continue to the next middleware in the chain
  // you can call "next" like this "next(data)" or simply like this "next()"
  return next();
});
domain.$('character').pre('save', (data, next) => {
  console.log('This hook/middleware runs before an entity is saved by the repository');

  // call "next" to continue to the next middleware in the chain
  // you can call "next" like this "next(data)" or simply like this "next()"
  return next(data);
});
domain.$('character').pre('save', (data, next) => {
  console.log('This hook/middleware alters the entity that is to be saved by the repository');

  return next({ ...data, field1: 'This field was added in a middleware' });
});
domain.$('character').on('save', () => console.log('This event handler runs after an entity is saved by the repository'));

Read more ➡️

Rules

Rules are functions that resolve to boolean values and are executed before certain repository operations. A resolved value of false means the rule is violated, and the repository operation should not continue.

Rules can be attached to a specific service or to the domain object, in which case they are available to every service in the domain.

import { Domain } from '@datadom/core';

const domain = new Domain();

async function entityMustHaveId(data) {
  return !!(data?.id);
}

domain.addRule('save', entityMustHaveId);

The rule entityMustHaveId will be executed whenever a repository wants to save an entity.

Read more ➡️