# Domain Repository

IDomainRepository is an extension of ORM frameworks (Mongoose or TypeORM) that **automatically maps DB models into Domain models**.

Useful links:

- [Why use it? Benefits and FAQ](https://github.com/lukaszwilisowski/domain-repository/blob/main/DISCUSSION.md)
- [API (similar to TypeORM repository)](https://github.com/lukaszwilisowski/domain-repository/blob/main/API.md)
- [How to contribute?](https://github.com/lukaszwilisowski/domain-repository/blob/main/CONTRIBUTION.md)
- [Code example](https://github.com/lukaszwilisowski/domain-repository-example)

## Installation with Mongoose

#1. First uninstall Mongoose and MongoDb.

```bash
npm uninstall mongoose mongodb @types/mongoose @types/mongodb
```

#2. Install latest Mongoose.

```bash
npm install mongoose
```

#3. Only AFTER you have latest Mongoose installed, install domain-repository.

```bash
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.

```bash
npm uninstall typeorm @types/typeorm
```

#2. Install latest TypeORM.

```bash
npm install typeorm
```

#3. Install domain-repository.

```bash
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 database
- `Attached` (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:

```typescript
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?)](https://github.com/lukaszwilisowski/domain-repository/blob/main/DISCUSSION.md#6-why-object-id-should-be-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.

```typescript
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:

```typescript
//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](https://www.npmjs.com/package/strict-type-mapper) npm package for more details.

```typescript
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.

```typescript
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).

```typescript
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:

```typescript
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:

```bash
{
  id: '63b8091cdd1f0c4927ca4725',
  name: 'Toyota',
  best: true,
  yearOfProduction: 2010,
  sold: 2023-01-06T11:42:20.836Z
}
```

MongoDB data (see best_of_all renamed property):

```json
{
  "_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:

```typescript
export type ICarSqlEntity = {
  id: number;
  name: string;
  best_of_all: boolean;
  readonly yearOfProduction: number;
  sold?: Date;
};
```

Db schema and mapping:

```typescript
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:

```typescript
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:

```bash
{
  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"
```
