@datadom/core
v0.1.8
Published
A simple library for representing business/data domains and models
Downloads
26
Maintainers
Readme
@datadom/core
This package contains the core elements of Datadom. These include domains, models, and rules.
For more info you can visit the project wiki.
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();
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.
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, Persisted } 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: ID): 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: ID): Promise<Persisted<ICharacter> | null> {
throw new Error('Method not implemented.');
}
getMany(params?: IQueryBuilder<ICharacter>): Promise<Persisted<ICharacter>[]> {
throw new Error('Method not implemented.');
}
save(data: SaveInput<ICharacter>): Promise<Persisted<ICharacter>> {
throw new Error('Method not implemented.');
}
update(id: ID, 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.');
}
}
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());
It is, however, possible to create a service without a concrete repository by calling domain.createService
.
This way, you can attach all the domain rules, hooks, and event listeners to the service without worrying
about the actual repository that will be associated with the service.
import { Domain } from '@datadom/core';
const domain = new Domain();
domain.createService('character');
Services created this way are associated with a default repository of type NullRepository
.
To associated a repository to a service created this way, simply call domain.registerRepository
.
The NullRepository
repository will be replaced with the provided repository, and all the service's
previously-added rules, hooks, and event listeners will continue to function as expected. This is
great for ensuring domain code remains decoupled from infrastructure code.
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'));
Rules
Rules are functions that resolve to boolean values and are executed before 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.