November 23, 2024

NestJS + RavenDB upload and store file attachments (best tutorial 10)

Hello and Happy New Year! Hope you all started the new year excited by the opportunities it brings with high hopes for a better world and exciting new things to learn!

Today I would like to show you a bit around uploading a file to an API and storing it as an attachment in RavenDB. I think it’s pretty important to know especially that nowadays every decent app requires some sort of file manipulation. There’s always the option of storing some sort of ID in the DB then saving the file locally, but RavenDB has file attachments on each document which can be leveraged in our case. Not exactly sure what is the file size limit or if storing a considerable amount of files would slow the database system in any way, but let’s try it out 😉

The project setting

In order to maintain clarity and continuity, I’m going to use the exact same project as with other RavenDB + NestJS tutorials which can be found on my Github page, tutorial10-fileupload branch.

Before diving into it, I had to add a new typing library for the File multer which is used further on:

$ yarn add --dev @types/multer

This should be already commited to the tutorial10-fileupload branch, but in case you have a project on your own and want to set it up don’t forget about this development dependency.

One note, if you are starting the database from scratch, you can use this backup to import your data for this tutorial.

Also, during the preparation of the code I also added some entity and repo classes into source control, I will skip them for brevity during this article. You will find all the source code needed in the Github project page.

Add attachment repository method

Magic wouldn’t work without a method that calls the database right? We will practically need this in our BaseRepo class so that it will be easy and elegant to call it from wherever we’d like. Let’s see how it looks like:


public async addAttachment(documentId: string, attachmentName: string, data: Buffer): Promise<void> {
  if (!await this.documentExists(documentId)) {
    throw `Document with ID ${documentId} does not exist!`;
  }
  const session = this.documentStore.openSession();
  session.advanced.attachments.store(documentId, attachmentName, data);
  await session.saveChanges();
  session.dispose();
}

This is my first shot at it, I’m curious how it will work. Practically, this method requires 3 parameters:

  1. The document ID for which we will attach the file
  2. The name of the file attachment
  3. The raw binary data of the file

Firstly, it will check that the document exists in our database. This is an async call that’s why the whole method has been defined as asynchronous.

Then it will open a RavenDB session and call the store method with the necessary 3 arguments. Afterwards the changes are saved and the session disposed of. This will clean all resources needed by the database context.

The magnificent API endpoint

We will call the addAttachment method from an API endpoint accessible from outside. The endpoint will be a POST and in the URL we will have our document ID. The body will be a form-data structure which will contain:

  1. file – the stream of raw bytes containing the file
  2. data – anything interesting related to our file. In this sample data is a JSON with a name property containing the file name

Let’s see how this whole method looks like:


@Post('attachment/:documentId')
@UseInterceptors(FileInterceptor('file'))
async addAttachment(@Param('documentId') documentId: string, @Body() body: any, @UploadedFile('file') file: Express.Multer.File) {
  const data = JSON.parse(body['data']);

  if (!file) {
    throw new BadRequestException('No file supplied!');
  }

  const buffer = file.buffer;
  const attachmentName = data['name'];

  try {
    await this.shopRepo.addAttachment(documentId, attachmentName, buffer);
  } catch (ex) {
    throw new BadRequestException(ex);
  }

  return {
    name: attachmentName,
  }
}

Again, this is my first shot at it and honestly I’m going to test it right now as I’m writing down 😀.

And yay! It worked by calling it from Postman with the following setup for the document with ID 99f91fc6-e5cd-42f4-a342-eb4df8174bbd:

Important: when you configure Postman you need to ensure that file is a File type and data is Text:

Now we can also check it in the RavenDB Studio:

Breaking it down

If the endpoint code seems overwhelming, don’t worry, I will explain it a bit since there’s some code specific to NestJS but backed by Express (NestJS is based on Express) under the hood.

The above annotations tell NestJS that we are dealing with an API endpoint which accepts POST requests on the route “attachment/:documentId“. The “:documentId” is a route parameter which will be parsed as an argument to our method in the definition. The “@UseInterceptors” thing tells NestJS that in the body the parameter “file” should be treated like a binary object.

The method definition contains practically 3 arguments:

  1. documentId – this is annotated with “@Param” which tells nest to pass the route parameter value of documentId in this variable
  2. body – This one is just annotated with “@Body” to get values of the HTTP body in the method for use. This is used to get the file name
  3. file – the “@UploadedFile” annotation helps us to retrieve the body value of “file” into it. This is defined as Express.Multer.File type as it relies on Express implementation under the hood

Here we parse the JSON data from the body and check for file existence. If there is no file we return a BadRequest 400 status code error.

In the above snipped we assign the necessary variables and we call the addAttachment method wrapped in a try catch clause. If there is any error (like document not found) we throw a BadRequest 400 error with details to the client.

Here we conclude our method by returning the attachment name back to the client. And, that would be it from this method, hope it shed some light about how it’s designed to work.

Conclusion

Need to attach files to documents in RavenDB? This got you covered! In my opinion, it’s very straightforward to do that and the SDK for NodeJS works like a breeze. If you are interested, I will do some tests with large files and see how it copes 😈. For now, I hope you found this useful and feel free to reach in case you have any questions or suggestions.

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 →