Service logic
Implementing the actual logic for managing the products' images.
Before entering the ProductsService
, we should add the FilesModule
to the imports
of the ProductsModule
. This way, the StorageService
will be available for injection in its context.
Now, let's actually implement these methods, starting with uploadImages()
. Let's begin by simply searching for the entity, but not doing anything with it. This is done just to check its existence.
await this.findOne(id);
We'll use certain path fragments many times, making it useful to have them as constants. So, let's create them in file.constants.
export const FilePath = {
Products: {
BASE: 'products',
IMAGES: 'images',
},
} as const satisfies Record<string, Record<string, string>>;
After that, back in the service, let's create the path
where the images will be saved.
const { BASE, IMAGES } = FilePath.Products;
const path = join(BASE, id.toString(), IMAGES);
Then, as we can save many images, let's ensure that the amount of images that arrived, combined with the amount already present in the directory, don't exceed the limit. However, we should first verify that the folder actually exists, lest an error will be thrown.
if (await pathExists(join(BASE_PATH, path))) {
const incomingFilecount = files.length;
const dirFilecount = await this.storageService.getDirFilecount(path);
const totalFilecount = incomingFilecount + dirFilecount;
this.storageService.validateFilecount(totalFilecount, MaxFileCount.PRODUCT_IMAGES);
}
We checked everything and have the path
in hand. Let's then create the directory where the files will be stored, in case it does not exist yet.
await this.storageService.createDir(path);
Finally, let's save the images in parallel using Promise.all()
.
await Promise.all(
files.map((file) => this.storageService.saveFile(path, file)),
);
Now, the downloadImage()
method. The step of searching for the entity is identical. However, the path
also includes the name of the file
to be downloaded.
const path = join(BASE, id.toString(), IMAGES, filename);
Then, it is checked if the file
actually exists.
await this.storageService.validatePath(path);
Finally, the file
is returned.
return this.storageService.getFile(path);
The last method is deleteImage()
, which is practically identical to downloadImage()
. The only difference is the last line, where the file is deleted.
await this.storageService.delete(path);
One final improvement: let's make that, when removing a product
, its file-related folder is too. First, create an auxiliary method that performs this exclusion.
private async deleteBaseDir(id: number) {
const { BASE } = FilePath.Products;
const path = join(BASE, id.toString());
await this.storageService.delete(path);
}
Then, invoke it in the remove()
method. However, so that both can happen in atomicity, we should wrap this in a transaction.
remove(id: number) {
return this.dataSource.transaction(async (manager) => {
const productsRepository = manager.getRepository(Product);
const product = await productsRepository.findOneByOrFail({ id });
await productsRepository.remove(product);
await this.deleteBaseDir(id);
return product;
});
}
We have finished implementing the file-related methods in the service.
Commit - Implementing file related methods in service
Last updated