December 22, 2024

Fastest Way to Implement a RavenDB Repository (NestJS + RavenDB tutorial 2)

Hello everybody and Happy New Year to you all! My best wishes for you and your loved ones in this new exciting year that just began.

Since I prepare for a trip, I’m eager to continue writing this RavenNest tutorial with my next topic that I want to touch: coding a RavenDB repository altogether with support for entities defined with Typescript. This pattern will be the base paradigm needed to access entities stored in database, query, patch or delete. I also want to cover the indexing topic, but it will be the subject of the future article.

Prerequisites

Before you continue, I want to make sure that you understand the basic app structure this tutorial is based on, which can be found in my Github repository. If you want to follow through, you’ll need to start from the tutorial1-yaml-config branch. The tutorial2-repository branch will contain the final code at the end of this article.

In this tutorial I assume you have some knowledge of Yarn, NodeJS and Typescript.

Defining entities

Cool, let’s now dig into it! The first step is to create a Repository module and the first this we are going to define inside will be the BaseEntity class which serves as the base all others. RavenDB will also need a descriptor definition in order to correctly map our entities to collections. Our new module called repository structure will look like this:

Our BaseEntity definition will be very simple:

export abstract class BaseEntity {
  id: string;

  public abstract get collectionName(): string;
}

Basically we defined an abstract class which contains the document (entity) id and an abstract property which will return the collection name inside RavenDB. The latter will be used in the descriptor. Let’s now create a sample Movie entity by extending this base class we just defined:

import { BaseEntity } from './base.entity';

export class MovieEntity extends BaseEntity {
  get collectionName(): string {
    return 'Movies';
  }

  public name: string;
  public year: number;
  public tags: string[];
}

So, we now have our first entity. Next we will dig into defining the the base repository and the PersistenceService.

The BaseRepo class

In this section, I will show you how to code a BaseRepo class which will stand as the base paradigm to access entities. Every entity will need to have a corresponding implementation of the repo class. Our BaseRepo class will define standard CRUD operations with the possibility of extension to searches, pagination, indexes and so forth.

import { BaseEntity } from '../entities';
import DocumentStore, { ObjectTypeDescriptor } from 'ravendb';

export class DocumentInterface<TEntity extends BaseEntity> {
  constructor(
    protected readonly documentStore: DocumentStore,
    protected readonly descriptor: {
      class: ObjectTypeDescriptor<TEntity>;
      collection: string;
    },
  ) {}

  public async storeDocument(entity: TEntity): Promise<TEntity> {
    const session = this.documentStore.openSession();

    const id = entity.id;

    entity['@metadata'] = {
      ['@collection']: this.descriptor.collection,
    };

    await session.store(entity, id);
    await session.saveChanges();

    session.dispose();

    return entity;
  }

  public async retrieveDocuments(): Promise<TEntity[]> {
    const session = this.documentStore.openSession();

    const results = await session
      .query({
        collection: this.descriptor.collection,
        documentType: this.descriptor.class,
      })
      .all();

    session.dispose();

    return results.map(this.metadataRemove);
  }
}

For brevity, I excluded the getById and delete operations; you can find them in the Github repository. Cool, now that we have this class defined, we can now derive a new repo class for our Movie entity:

import { BaseRepo } from './base.repo';
import { MovieEntity } from '../entities';

export class MovieRepo extends BaseRepo<MovieEntity> {}

Unless you want to add some custom methods in it, the MovieRepo class is good to go.

Our mighty PersistenceService

Now after defining our entities and repos, we reached the point where we need some class to help centralize all these things. Our repos are not defined as injectable, so a bit of help and logic is needed, hence this mighty PersistenceService class will be born 🥸:

@Injectable()
export class PersistenceService {
  private readonly documentStore: DocumentStore;
  private readonly descriptorsByCollection = {};
  private readonly descriptorsByName = {};
  private readonly documentInterfaces = {};

  constructor(private readonly config: ConfigService) {
    if (this.config.get('db.raven.secure')) {
      const authSettings: IAuthOptions = {
        certificate: fs.readFileSync(this.config.get('db.raven.certificate')),
        type: 'pfx',
        password: this.config.get('db.raven.passphrase'),
      };

      this.documentStore = new DocumentStore(
        this.config.get('db.raven.url'),
        this.config.get('db.raven.database'),
        authSettings,
      );
    } else {
      this.documentStore = new DocumentStore(
        this.config.get('db.raven.url'),
        this.config.get('db.raven.database'),
      );
    }

    entityDescriptor.forEach((descriptor) => {
      this.documentStore.conventions.registerEntityType(
        descriptor.class,
        descriptor.collection,
      );
      if (this.descriptorsByCollection[descriptor.collection]) {
        throw `Collection name ${descriptor.collection} already in use`;
      } else {
        this.descriptorsByCollection[descriptor.collection] = descriptor;
        this.descriptorsByName[descriptor.name] = descriptor;
      }
    });
    this.documentStore.initialize();
  }

  public getMovieRepo(): MovieRepo {
    if (!this.documentInterfaces[MovieEntity.name]) {
      this.documentInterfaces[MovieEntity.name] = new MovieRepo(
        this.documentStore,
        this.descriptorsByName[MovieEntity.name],
      );
    }
    return this.documentInterfaces[MovieEntity.name];
  }
}

Holistically, this class is responsible for:

  1. Initializing the connection to RavenDB
  2. Registering each entity type in the descriptor to the database
  3. Defining methods for accessing each entity repository

As this class is defined as Injectable, we need to add it in the module providers:

@Module({
  imports: [],
  providers: [PersistenceService],
  exports: [PersistenceService],
})
export class RepositoryModule {}

Testing it up

All right, let’s now start the engine and see how our work performs. The plan is to create a new controller with 3 capabilities:

  1. List the movies in database
  2. Get a movie by ID
  3. Create a new movie entity

This code is quite trivial, no funky stuff going on there. This is the beauty of keeping the concepts separate and clean respecting the SOLID principles as well: your code is “breathable” and not tied up to specific implementations. If you want to call repo functions from elsewhere, it’s totally fine and straightforward.

@Controller('movie')
export class MovieController {
  private readonly movieRepo: MovieRepo;

  constructor(readonly persistence: PersistenceService) {
    this.movieRepo = persistence.getMovieRepo();
  }

  @Get()
  async getMovies() {
    return await this.movieRepo.retrieveDocuments();
  }

  @Get(':id')
  async getMovieById(@Param('id') id) {
    const movie = await this.movieRepo.getById(id);

    if (movie) {
      return movie;
    }

    throw new NotFoundException();
  }

  @Post()
  async createMovie(@Body() body: MovieEntity) {
    const created = await this.movieRepo.storeDocument(body);
    return created;
  }
}

Now by using Postman we can test these endpoints:

Movie creation works just fine, let’s see if we can list all the movies (sorry for lack of inspiration):

Works like a breeze, Yuhuu! So I guess that’s it for the purpose of this tutorial. 😀

Wrapping it up

We’ve just learned something really precious in this tutorial, having a clean way to define a repository for our RavenDB connection. This is really important, as the application grows it will not be hindered by persistence problems and you can concentrate on business logic. The methods defined in this repository can be extended at your own wish depending on your needs. I certainly think this is a good start for a maintainable application. In the future, I want to cover more about RavenDB indexes, unit testing and other best practices, so stay tuned!

Thanks for reading, I hope you found this article useful and interesting. If you have any suggestions don’t hesitate to contact me. If you found my content useful please consider a small donation. Any support is greatly appreciated! Cheers  😉

afivan

Enthusiast adventurer, software developer with a high sense of creativity, discipline and achievement. I like to travel, I like music and outdoor sports. Because I have a broken ligament, I prefer safer activities like running or biking. In a couple of years, my ambition is to become a good technical lead with entrepreneurial mindset. From a personal point of view, I’d like to establish my own family, so I’ll have lots of things to do, there’s never time to get bored 😂

View all posts by afivan →