Skip to content
/ domain Public

A lightweight, type-safe Domain layer for TypeScript. Features reactive Entities with automatic dirty checking, smart Repositories, and clean architecture patterns.

License

Notifications You must be signed in to change notification settings

ZyroHub/domain

Repository files navigation

ZyroHub - Domain

This package contains the domain logic of the applications. It defines the essential entities, and repositories that form the foundation of an application.

Table of Contents

Getting Started

To install the domain package, use one of the following package managers:

NPM Repository

# npm
npm install @zyrohub/domain
# yarn
yarn add @zyrohub/domain
# pnpm
pnpm add @zyrohub/domain
# bun
bun add @zyrohub/domain

Entities

Creating a Basic Entity

import { Entity } from '@zyrohub/domain';

export interface UserEntityRaw {
	id?: string; // optional id, for not existing users
	name: string;
	email: string;
	password: string;
}

export class UserEntity extends Entity<UserEntityRaw> {
	constructor(raw: UserEntityRaw) {
		super({
			data: raw
		});
	}

	// Filter out sensitive data or modify the output as needed (see section "#Customizing Entity Output with filterObject")
	filterObject(data: any, options: any): any {
		delete data.password; // Exclude password from the output

		return data;
	}
}

const user = new UserEntity({
	id: '1',
	name: 'John Doe',
	email: 'john.doe@example.com',
	password: 'securepassword'
});

// raw data: { id: '1', name: 'John Doe', email: 'john.doe@example.com', password: 'securepassword' }
// unwrap() returns the clean raw data (POJO) useful for database persistence, etc.
console.log(user.unwrap());

// processed data without password (to expose in public or other scenarios): { id: '1', name: 'John Doe', email: 'john.doe@example.com' }
console.log(user.toObject());

Custom ID Generation

import { Entity } from '@zyrohub/domain';
import { randomUUID } from 'crypto';

export interface UserEntityRaw {
	id?: string; // optional id, for not existing users
	name: string;
	email: string;
	password: string;
}

export class UserEntity extends Entity<UserEntityRaw> {
	constructor(raw: UserEntityRaw) {
		super({
			idGenerator: () => randomUUID(), // Custom ID generator for non-existing users
			data: raw
		});
	}
}

Entity Relations

You can define relations between entities by creating interfaces for the related entities and including them in the main entity's raw data and relations.

import { Entity } from '@zyrohub/domain';

import { UserEntity, UserEntityRaw } from './UserEntity.js';

// Post entity without relations
export interface PostEntityData {
	id: string;
	title: string;
	content: string;
	authorId: string;
}

// Post entity with relations merged
export interface PostEntityRaw extends PostEntityData {
	author?: UserEntityRaw;
}

// Post entity relations interface
export interface PostEntityRelations {
	author?: UserEntity;
}

export class PostEntity extends Entity<PostEntityRaw, PostEntityRelations> {
	constructor(raw: PostEntityRaw) {
		// Destructure to separate relations from raw data
		const { author, ...data } = raw;

		super({
			data: data,
			// Map relations to their respective entity instances
			relations: {
				author: author ? new UserEntity(author) : undefined
			}
		});
	}
}

const post = new PostEntity({
	id: '101',
	title: 'My First Post',
	content: 'Content',
	authorId: '1',
	author: {
		id: '1',
		name: 'John Doe',
		email: 'john.doe@example.com'
	}
});

console.log(post.toObject());
// Output: {
//  id: '101',
//  title: 'My First Post',
//  content: 'Content',
//  authorId: '1',
//  relations: {
//    author: { id: '1', name: 'John Doe', email: 'john.doe@example.com' }
//  }
// }

Customizing Entity Output with filterObject

You can customize the output of an entity by overriding the filterObject method. This allows you to modify the data before it is returned by the toObject method.

import { Entity, EntityToObjectOptions } from '@zyrohub/domain';

export interface UserEntityRaw {
	id: string;
	name: string;
	email: string;
	password: string;
	age: number;
}

export interface UserEntityToObjectOptions extends EntityToObjectOptions {
	// view: string; is automatically included in EntityToObjectOptions interface (you can use "admin", "public", etc. if needed)

	// Add any additional options specific to UserEntity here
	includeAge?: boolean;
}

export class UserEntity extends Entity<UserEntityRaw> {
	constructor(raw: UserEntityRaw) {
		super({
			data: raw
		});
	}

	filterObject(data: any, options: UserEntityToObjectOptions): any {
		// Exclude password from the output
		delete data.password;

		// Conditionally include age based on options
		if (!options.includeAge) {
			delete data.age;
		}

		// Using different views (if needed)
		if (options.view !== 'admin') {
			delete data.email; // Exclude email in public view
		}

		return data;
	}
}

// Example usage
const user = new UserEntity({
	id: '1',
	name: 'John Doe',
	email: 'john.doe@example.com',
	password: 'securepassword',
	age: 30
});

// Default view without age
console.log(user.toObject({ view: 'default' })); // { id: '1', name: 'John Doe' }
// Default view with age
console.log(user.toObject({ view: 'default', includeAge: true })); // { id: '1', name: 'John Doe', age: 30 }
// Admin view with age
console.log(user.toObject({ view: 'admin', includeAge: true })); // { id: '1', name: 'John Doe', email: 'john.doe@example.com', age: 30 }
// Admin view without age
console.log(user.toObject({ view: 'admin' })); // { id: '1', name: 'John Doe', email: 'john.doe@example.com' }

Collecting list of changes

The Entity class tracks changes made to its data. You can retrieve the list of changes using the getChanges method and reset the changes tracker with the commit method.

import { UserEntity } from './UserEntity.js';

const user = new UserEntity({
	id: '1',
	name: 'John Doe',
	email: 'john.doe@example.com',
	password: 'securepassword',
	age: 30
});

user.data.name = 'Jane Doe';
user.data.age = 31;

console.log(user.getChanges()); // { name: 'Jane Doe', age: 31 }

// do something with the changes...

user.commit(); // resets the changes tracker

Group of Entities (EntityGroup)

You can manage a collection of entities using the EntityGroup class. This class provides methods to manipulate and access multiple entities as a group.

⚠️ Note: The EntityGroup class is a extension of the native JavaScript Array class, so you can use all standard array methods on it. (e.g., map, filter, forEach, etc.)

import { EntityGroup } from '@zyrohub/domain';

import { UserEntity } from './UserEntity.js';

// Creating an EntityGroup from a list of UserEntity instances
const usersGroupFromEntities = EntityGroup.fromList([
	new UserEntity({
		id: '1',
		name: 'John Doe',
		email: 'john.doe@example.com',
		password: 'securepassword',
		age: 30
	}),
	new UserEntity({
		id: '2',
		name: 'Jane Smith',
		email: 'jane.smith@example.com',
		password: 'anothersecurepassword',
		age: 25
	})
]);

// Creating a new EntityGroup directly from raw data (database data)
// Entity automatically add the fromList static method to create groups from raw data
const usersGroupFromRaw = UserEntity.fromList([
	{
		id: '1',
		name: 'John Doe',
		email: 'john.doe@example.com',
		password: 'securepassword',
		age: 30
	},
	{
		id: '2',
		name: 'Jane Smith',
		email: 'jane.smith@example.com',
		password: 'anothersecurepassword',
		age: 25
	}
]);

Getting an Entity by ID

You can retrieve an entity from the group by its ID using the getById method.

const user = usersGroup.getById('1');

console.log(user); // UserEntity instance for John Doe

Removing an Entity by ID

You can remove an entity from the group by its ID using the removeById method.

usersGroup.removeById('2');

console.log(usersGroup.length); // 1 (only John Doe remains)

Repositories

Defining the Repository Interface

First, define the repository interface for your entity by extending the generic Repository interface from the domain package.

import { Repository } from '@zyrohub/domain';

import { UserEntity } from './UserEntity.js';

export interface IUserRepository extends Repository<UserEntity> {
	// "Repository" interface already includes as required the methods:
	// create(entity: TEntity): Promise<void>;
	// update(entity: TEntity): Promise<void>;
	// delete(id: EntityId): Promise<boolean>;

	// save(entity: TEntity): Promise<void>; is already included in BaseRepository class

	// Define custom methods for UserRepository here
	findByEmail(email: string): Promise<UserEntity | null>;
}

Creating a Repository

Then, implement the repository by extending the BaseRepository class and implementing your repository interface.

import { BaseRepository } from '@zyrohub/domain';

import { IUserRepository } from './IUserRepository.js';
import { UserEntity } from './UserEntity.js';

// An example implementation of UserRepository using an in-memory array
export class InMemoryUserRepository extends BaseRepository<UserEntity> implements IUserRepository {
	// In-memory user storage (for demonstration purposes)
	private users: UserEntity[] = [];

	// Required method from Repository interface
	async create(entity: UserEntity): Promise<void> {
		this.users.push(entity.unwrap()); // store raw data of the entity
		entity.commit(); // commit changes after creation
		entity.exists = true; // mark entity as existing
	}

	// Required method from Repository interface
	async update(entity: UserEntity): Promise<void> {
		const index = this.users.findIndex(user => user.id === entity.id);

		if (index !== -1) {
			this.users[index] = {
				...this.users[index],

				...entity.getChanges() // apply only changes
			};

			entity.commit(); // commit changes after update
		} else {
			throw new Error('User not found');
		}
	}

	// Required method from Repository interface
	async delete(id: string): Promise<boolean> {
		this.users.splice(
			this.users.findIndex(user => user.id === id),
			1
		);

		return true;
	}

	// Your custom methods from UserRepository interface
	async findByEmail(email: string): Promise<UserEntity | null> {
		const user = this.users.find(user => user.data.email === email);

		return user || null;
	}
}

In Memory Repository

The InMemoryRepository class provides a simple in-memory data management solution for entities. It extends the BaseRepository class and implements basic CRUD operations using an internal array to store entities.

import { InMemoryRepository } from '@zyrohub/domain';

import { IUserRepository } from './IUserRepository.js';
import { UserEntity } from './UserEntity.js';

export class InMemoryUserRepository extends InMemoryRepository<UserEntity> implements IUserRepository {
	// create, update, delete and save methods are already implemented in InMemoryRepository automatically
	// also add some useful methods like "clear"(to use in tests), etc.

	// Your custom methods from UserRepository interface
	async findByEmail(email: string): Promise<UserEntity | null> {
		const user = this.items.find(user => user.data.email === email);

		return user || null;
	}
}

Repository Example Usage

Save

The BaseRepository class provides a save method that automatically detects whether to create a new entity or update an existing one based on the entity's exists property.

import { InMemoryUserRepository } from './InMemoryUserRepository.js';

const userRepository = new InMemoryUserRepository();

const user = new UserEntity({
	id: '1',
	name: 'John Doe',
	email: 'john.doe@example.com',
	password: 'securepassword',
	age: 30
});

await userRepository.save(user); // auto-detects create or update using "entity.exists" property

Create

You can create a new entity using the create method provided by the BaseRepository class. (if you want to explicitly create without save auto-detection)

import { InMemoryUserRepository } from './InMemoryUserRepository.js';

const userRepository = new InMemoryUserRepository();

const user = new UserEntity({
	name: 'John Doe',
	email: 'john.doe@example.com',
	password: 'securepassword',
	age: 30
});

const createdUser = await userRepository.create(user); // returns the created UserEntity instance if you want to use it

Update

You can update an existing entity using the update method provided by the BaseRepository class. (if you want to explicitly update without save auto-detection)

import { InMemoryUserRepository } from './InMemoryUserRepository.js';

const userRepository = new InMemoryUserRepository();

const user = await userRepository.findByEmail('john.doe@example.com');

if (user) {
	user.data.name = 'Jane Doe';

	await userRepository.update(user); // returns the updated UserEntity instance if you want to use it
}

Delete

You can delete an entity using the delete method provided by the BaseRepository class.

import { InMemoryUserRepository } from './InMemoryUserRepository.js';

const userRepository = new InMemoryUserRepository();

const success = await userRepository.delete('1'); // returns true if deletion was successful

About

A lightweight, type-safe Domain layer for TypeScript. Features reactive Entities with automatic dirty checking, smart Repositories, and clean architecture patterns.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published