domain-repository
v2.0.12
Published
IDomainRepository is an extension of ORM frameworks (Mongoose or TypeORM) that **automatically maps DB models into Domain models**.
Downloads
536
Maintainers
Readme
Domain Repository
IDomainRepository is an extension of ORM frameworks (Mongoose or TypeORM) that automatically maps DB models into Domain models.
Useful links:
Installation with Mongoose
#1. First uninstall Mongoose and MongoDb.
npm uninstall mongoose mongodb @types/mongoose @types/mongodb
#2. Install latest Mongoose.
npm install mongoose
#3. Only AFTER you have latest Mongoose installed, install domain-repository.
npm install domain-repository
If you do it the other way round, you can have problems with MongoDB BSON dependency!
Installation with TypeORM
#1. First uninstall TypeORM.
npm uninstall typeorm @types/typeorm
#2. Install latest TypeORM.
npm install typeorm
#3. Install domain-repository.
npm install domain-repository
How to use it
1. Define your domain models
Make sure you have domain models defined. Each model should be exported in two versions:
Detached
(default model without id), for objects not yet persisted in the databaseAttached
(with id), for already persisted objects
This differentiation improves intellisense and debugging. You can call your models whatever you like, as long as you stick to your naming convention. We recommend the following good practices:
- use IType naming convention for pure model types, to distinguish them from classes
- add Attached suffix to all of your attached models
- provide JSDoc comments to your model properties (so that your developers can better understand the domain)
- add mutable / readonly prefix to your property descriptions
- if property is optional, explain why it is optional (due to business, technological or temporary reasons)
For example:
export type ICar = {
/* Mutable name of the car. */
name: string;
/* Mutable flag. Equals true for the single, highest ranked car in the system. */
best: boolean;
/* Readonly year of production. */
readonly yearOfProduction: number;
/* Mutable sale date. Optional means the car was not sold yet. */
sold?: Date;
};
export type ICarAttached = ICar & { id: string };
An attached model will contain:
- at minimum, a string Id (why Id is of type string?)
- other properties auto-generated by the db engine (not manually assignable)
2. Define your DB models (in Mongoose or TypeORM).
Let's say you want to use MongoDB as your DB, and Mongoose as your ORM.
Create a new file for my DB model, for example: car.entity.ts
:
Because this library uses domain mapping, this model does not have to be the same as your domain model.
export type ICarMongoEntity = {
_id: mongoose.Types.ObjectId;
name: string;
best_of_all: boolean;
readonly yearOfProduction: number;
sold?: Date;
};
Now create file car.schema.ts
and define your db schema, using mongoose:
//standard MongoDb schema, typed with your db model
export const CarSchema = new Schema<ICarMongoEntity>({
name: {
type: String,
required: true
},
best_of_all: {
type: Boolean,
required: true
},
yearOfProduction: {
type: Number,
required: true
},
sold: {
type: Date,
required: false
}
});
3. Define mapping between Domain and DB models
Check strict-type-mapper npm package for more details.
import { Mapping } from 'strict-type-mapper';
import { mapToMongoObjectId } from 'domain-repository/db/mongodb';
//define mapping from domain model to db model
export const mongoCarMapping: Mapping<ICarAttached, ICarMongoEntity> = {
id: mapToMongoObjectId,
name: 'name',
best: 'best_of_all',
yearOfProduction: 'yearOfProduction',
sold: 'sold'
};
4. Use IDomainRepository in your business services
Use IDomainRepository
interface in places, where you would previously use Mongoose collection or TypeORM repository. Type it explicitly with your Domain model type.
import { IDomainRepository } from 'domain-repository';
export class CarService {
constructor(private readonly carRepository: IDomainRepository<ICar, ICarAttached>) {}
public async create(car: ICar): Promise<ICarAttached> {
return this.carRepository.create(car);
}
public async findBestCar(): Promise<ICarAttached | undefined> {
return this.carRepository.findOne({ best: true });
}
}
If you only need to read or write data you can also use narrowed versions of interfaces: IReadDomainRepository
or IWriteDomainRepository
(SOLID's Interface segregation principle).
5. Write unit tests (Test-driven-development)
Here lies the greatest benefit of using IDomainRepository. You can easily test your services using MockedDbRepository implementation.
No more difficult mocking of db methods!
This way you can focus on your business code and test only that (this is one of the principal guidelines of unit testing).
import { MockedDBRepository } from 'domain-repository';
describe('CarService', () => {
const initialData: ICarAttached[] = [
{ id: '1', name: 'Volvo', best: false, yearOfProduction: 2000 },
{
id: '2',
name: 'Toyota',
best: true,
yearOfProduction: 2010,
sold: new Date()
}
];
const mockedRepository = new MockedDBRepository<ICar, ICarAttached>(initialData);
const carService = new CarService(mockedRepository);
it('should find best car', async () => {
const car = await carService.findBestCar();
expect(car).toBeDefined();
expect(car!.name).toEqual('Toyota');
});
});
6. Supply your services with proper repository implemenation for your target DB.
Now depending on your db and ORM layer, you need to create ORM repository and pass it to our implementation of IDomainRepository.
MongoDb example:
import { MongoDbRepository } from 'domain-repository/db/mongodb';
const runMongoTest = async (): Promise<void> => {
await new Promise<void>((resolve) => {
mongoose.connect('mongodb://127.0.0.1:27017/testdb', {});
mongoose.connection.on('open', () => resolve());
});
const carRepository = new MongoDbRepository<ICar, ICarAttached, ICarMongoEntity>(
mongoose.model<ICarMongoEntity>('cars', CarSchema),
mongoCarMapping
);
const carService = new CarService(carRepository);
await carService.create({
name: 'Toyota',
best: true,
yearOfProduction: 2010,
sold: new Date()
});
const bestCar = await carService.findBestCar();
console.log(bestCar);
};
runMongoTest();
Output:
{
id: '63b8091cdd1f0c4927ca4725',
name: 'Toyota',
best: true,
yearOfProduction: 2010,
sold: 2023-01-06T11:42:20.836Z
}
MongoDB data (see best_of_all renamed property):
{
"_id": {
"$oid": "63b8091cdd1f0c4927ca4725"
},
"name": "Toyota",
"best_of_all": true,
"yearOfProduction": 2010,
"sold": {
"$date": "2023-01-06T11:42:20.836Z"
},
"__v": 0
}
PostgreSQL example
Db model:
export type ICarSqlEntity = {
id: number;
name: string;
best_of_all: boolean;
readonly yearOfProduction: number;
sold?: Date;
};
Db schema and mapping:
import { Mapping } from 'strict-type-mapper';
import { mapToSqlIntId } from 'domain-repository/db/postgresql';
//you can put ! next to the properties, to prevent Typescript no-initializer warnings
@Entity('cars')
export class SqlCarEntity implements ICarSqlEntity {
@PrimaryGeneratedColumn()
readonly id!: number;
@Column('text')
name!: string;
@Column('bool')
best_of_all!: boolean;
@Column('int')
readonly yearOfProduction!: number;
@Column('text', { nullable: true })
sold?: Date;
}
export const sqlCarMapping: Mapping<ICarAttached, ICarSqlEntity> = {
id: mapToSqlIntId,
name: 'name',
best: 'best_of_all',
yearOfProduction: 'yearOfProduction',
sold: 'sold'
};
Test code:
import { PostgreSQLDbRepository } from 'domain-repository/db/postgresql';
const runPostgresTest = async (): Promise<void> => {
const dataSource = new DataSource({
type: 'postgres',
host: '127.0.0.1',
port: 5432,
database: 'mydb',
username: 'postgres',
password: 'admin',
synchronize: true, //for local testing
entities: [SqlCarEntity]
});
await dataSource.initialize();
const carRepository = new PostgreSQLDbRepository<ICar, ICarAttached, ICarSqlEntity>(
dataSource.getRepository(SqlCarEntity),
sqlCarMapping
);
const carService = new CarService(carRepository);
await carService.create({
name: 'Toyota',
best: true,
yearOfProduction: 2010,
sold: new Date()
});
const bestCar = await carService.findBestCar();
console.log(bestCar);
};
runPostgresTest();
Output:
{
id: '146',
name: 'Toyota',
best: true,
yearOfProduction: 2010,
sold: '2023-01-06T13:11:43.685+01:00'
}
PostgreSQL data (see best_of_all renamed property):
id,"name","best_of_all","yearOfProduction","sold"
146,"Toyota",True,2010,"2023-01-06T13:11:43.685+01:00"