Use Interceptor inside another Interceptor In nestjs - node.js

I am creating a interceptor Inside that I want to use FileInterceptor but I am getting error Declaration Expected after FileInterceptor
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, UseInterceptors } from '#nestjs/common';
import { Observable } from 'rxjs';
import { FileInterceptor } from '#nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
#Injectable()
export class UploaderInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
#UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: './uploads'
, filename: (req, file, cb) => {
// Generating a 32 random chars long string
const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('')
//Calling the callback passing the random name generated with the original extension name
cb(null, `${randomName}${extname(file.originalname)}`)
}
})
}));
return next.handle();
}
}

FileInterceptor is a mixin meaning it is a function that returns a class. This function takes in a configuration for the interceptor to actually use. What you can do instead of trying to make a class that makes use of the interceptor under the hood or extends it, is essentially make an alias for the configuration like so:
export const UploadInterceptor = FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
// Generating a 32 random characters long string
const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('')
//Calling the callback passing the random name generated with the original extension name
cb(null, `${randomName}${extname(file.originalname)}`)
}
})
})
Now with this exported constant (that is actually a class), you can then make use of the interceptor as such:
#Controller('upload')
export class UploadController {
#UseInterceptors(UploadInterceptor)
#Post()
uploadFile() {
// do logic;
}
}
And everything should work out for you.

Related

I created custom nest js guard which is running before every request and I have added a property called all() on the request in that guard

I have created a interface which is extending the request (of express)
I m adding property called all() in it
which contains the body , query and params in it
import { Request as BaseRequest } from 'express';
export interface Request extends BaseRequest {
all(): Record<string, any>;
}
this is the interface
which is extending the express request
and i m adding this all() property using the guard
this is the implementation of it
#Injectable()
export class RequestGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
this.requestHelper(context.switchToHttp().getRequest());
return true;
}
requestHelper(request: any): any {
const all = function (): Record<string, any> {
return {
...request.query,
...request.body,
...request.params,
};
};
request.all = all;
return request;
}
}
in the main.ts file i have used this guard
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '#nestjs/common';
import { RequestGuard } from './core/guards/request.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
app.useGlobalGuards(new RequestGuard());
await app.listen(3000);
}
bootstrap();
and i have tried consoling the all() property in the guard and it's working
its mean request is flowing in it
when i try to get this all() property in my controller then it showing
Cannot read properties of undefined (reading 'all')
That's how i m calling it
import {
Controller,
Get,
Post,
Param,
Body,
Req,
Res,
UseGuards,
} from '#nestjs/common';
import { RequestGuard } from 'src/core/guards/request.guard';
import { Request } from 'src/core/http/Request';
import { Response } from 'src/core/http/Response';
#UseGuards(RequestGuard)
#Controller('customers')
export class CustomersController {
constructor(private customersService: CustomersService) {}
#Get('/order-data/:id')
async OrderData(#Param('id') id: string, req: Request, #Res() res: Response) {
console.log(req.all());
const data = await this.customersService.allOrdersData(parseInt(id));
return data;
}
}
I m calling the route localhost:3000/customers/order-data/1
console.log(req.all());
It should print {id:{'1'}}
But it's giving error
Cannot read properties of undefined (reading 'all')
You're missing the #Req() for your req property in the OrderData method.

"then" is not exuceted after a Promise<void> when writing a file from a POST Request

I have currently an express server. I am trying to make a POST request without success.
Here is my ccontroller :
import { BAD_REQUEST } from '#app/constant';
import { SaveDrawService } from '#app/services/save-draw.service';
import { TYPES } from '#app/types';
import { Image } from '#common/communication/Image';
import { NextFunction, Request, Response, Router } from 'express';
import { inject, injectable } from 'inversify';
import 'reflect-metadata';
#injectable()
export class SaveDrawController {
router: Router;
constructor(#inject(TYPES.SaveDrawService) private saveDrawService: SaveDrawService) {
this.configureRouter();
}
private configureRouter(): void {
this.router = Router();
this.router.post('/write', (req: Request, res: Response, next: NextFunction) => {
if (!req.body) return res.sendStatus(BAD_REQUEST);
this.saveDrawService.writeData(req.body as Image);
return res.sendStatus(this.saveDrawService.code);
});
this.router.get('/read', (req: Request, res: Response, next: NextFunction) => {
return res.send(this.saveDrawService.readImageData());
});
}
}
Image here is a interface that i want to POST with these parameters:
export interface Image {
title: string;
tags: string[];
data: string; // base64 image from HTML canvas
}
Here is my service where I try to write the file :
import { ERROR, OK } from '#app/constant';
import { Image } from '#common/communication/Image';
import { readFile, writeFile } from 'fs';
import { injectable } from 'inversify';
import 'reflect-metadata';
import * as util from 'util';
#injectable()
export class SaveDrawService {
code: number;
constructor() {}
async writeData(image: Image): Promise<void> {
const base64Data = image.data.replace('data:image/png;base64,', '');
const write = util.promisify(writeFile);
return await write('test.png', base64Data, 'base64')
.then(() => {
this.code = OK; // 200
})
.catch((error: Error) => {
console.error(error);
this.code = ERROR; // 500
});
}
async readImageData(): Promise<string> {
const read = util.promisify(readFile);
return await read('test.png', { encoding: 'base64' });
}
extractFormat(base64Data: string) {}
}
The problem is that the "then" in write is not executed after the write and the "this.code" is therefore never updated and makes the request crash. I just started and I really don't know what can be causing this.
Here is my request I make to test the code:
On my server the POST is received and my server log this :
POST /api/draw/write 500 20.825 ms - 92
UPDATE: both my GET and POST return a error, but they are writing and reading the file on the server (I verify by making a POST and after a GET with logs to see if they are the same)
I think this is what you should change. Don't use async/await with then/catch, these are two different notation to wait for asynchronous code and get data.
async writeData(image: Image): Promise<void> {
const base64Data = image.data.replace('data:image/png;base64,', '');
const write = util.promisify(writeFile);
const resp = await write('test.png', base64Data, 'base64');
if (resp.ok) // whatever your condition
{
this.code = OK;
} else {
console.error(resp.error); // show error here
this.code = ERROR;
}
}
Check here for more details.

NestJS: Receive form-data in Guards?

I'm looking to see form-data in my NestJS Guards. I've followed the tutorial, however, I'm not seeing the request body for my form-data input. I do see the body once I access a route within my controller, however.
Here's some code snippets of what I'm working with:
module.ts
...
#Module({
imports: [
MulterModule.register({
limits: { fileSize: MULTER_UPLOAD_FILESIZE_BYTES },
}),
],
controllers: [MainController],
providers: [
MainService,
AuthGuard,
],
})
...
AuthGuard.ts
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Observable } from 'rxjs';
#Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest(); // body is empty if form-data is used
return true;
}
}
MainController.ts
...
#Post("/upload")
#UseInterceptors(AnyFilesInterceptor())
#UseGuards(AuthGuard)
async upload(
#Body() body: UploadDTO,
#UploadedFiles() files: any[]
): Promise<any> {
console.log(body) // works as expected, whether form-data is used or not
...
}
...
Any feedback would be greatly appreciated!
NestJS guards are always executed before any middleware. You can use multer manually on the request object you get from the context.
import * as multer from 'multer'
...
async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const postMulterRequest = await new Promise((resolve, reject) => {
multer().any()(request, {}, function(err) {
if (err) reject(err);
resolve(request);
});
});
// postMulterRequest has a completed body
return true;
}
If you want to use the #UploadedFiles decorator, you need to clone the request object before modifying it in your guard.
Of course you need to have installed the multer module with:
npm install multer
Posting my solution in-case it helps other devs dealing with the same issue.
To start, I created a middleware to handle the conversion of the multipart form data request. You could also inline this in to your guard if you only have one or two. Much of this code is plagiarised from the source code, and is not fully tested:
const multerExceptions = {
LIMIT_PART_COUNT: 'Too many parts',
LIMIT_FILE_SIZE: 'File too large',
LIMIT_FILE_COUNT: 'Too many files',
LIMIT_FIELD_KEY: 'Field name too long',
LIMIT_FIELD_VALUE: 'Field value too long',
LIMIT_FIELD_COUNT: 'Too many fields',
LIMIT_UNEXPECTED_FILE: 'Unexpected field',
}
function transformException(error: Error | undefined) {
if (!error || error instanceof HttpException) {
return error
}
switch (error.message) {
case multerExceptions.LIMIT_FILE_SIZE:
return new PayloadTooLargeException(error.message)
case multerExceptions.LIMIT_FILE_COUNT:
case multerExceptions.LIMIT_FIELD_KEY:
case multerExceptions.LIMIT_FIELD_VALUE:
case multerExceptions.LIMIT_FIELD_COUNT:
case multerExceptions.LIMIT_UNEXPECTED_FILE:
case multerExceptions.LIMIT_PART_COUNT:
return new BadRequestException(error.message)
}
return error
}
#Injectable()
export class MultipartMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
// Read multipart form data request
// Multer modifies the request object
await new Promise<void>((resolve, reject) => {
multer().any()(req, res, (err: any) => {
if (err) {
const error = transformException(err)
return reject(error)
}
resolve()
})
})
next()
}
}
Then, I applied the middleware conditionally to any routes which accept multipart form data:
#Module({
controllers: [ExampleController],
imports: [...],
providers: [ExampleService],
})
export class ExampleModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(MultipartMiddleware).forRoutes({
path: 'example/upload',
method: RequestMethod.POST,
})
}
}
Finally, to get the uploaded files, you can reference req.files:
#Controller('example')
export class ExampleController {
#Post('upload')
upload(#Req() req: Request) {
const files = req.files;
}
}
I expanded this in my own codebase with some additional supporting decorators:
export const UploadedAttachment = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.files?.[0]
}
)
export const UploadedAttachments = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.files
}
)
Which ends up looking like:
#Controller('example')
export class ExampleController {
#Post('upload')
upload(#UploadedAttachments() files: Express.Multer.File[]) {
...
}
}

Uploading File Error using Fastify and Nestjs

I am trying to upload a file to my nest.js server but I am getting an error:
Error: Unsupported Media Type: multipart/form-data; boundary=--------------------------140603536005099484714904
I followed this documentation.
Angular 6 Code
public async Upload<TReponse>(file: File, path) {
try {
const formData = new FormData();
formData.append("file", file);
//>> function to upload and applying header as null
const result = await this.http.post(this.baseUrl + path, formData, { headers: null }).pipe(map((response: any) => response)).toPromise();
var response = result as ApiResponseObject<TReponse>;
return response;
}
catch (ex) {
var result = new ApiResponseObject<TReponse>()
result.message = ex.message;
result.isSuccess = false;
return result;
}
finally {
}
}
NestJs Code Module Code
import { Module, NestModule, MiddlewareConsumer, MulterModule } from '#nestjs/common';
import { UserService } from './services/user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '#nestjs/typeorm';
import { User } from './dto/user.entity';
import * as multer from 'multer';
#Module({
imports: [TypeOrmModule.forFeature([User]),
MulterModule.register({
dest: '/public',
fileFilter: (req, file, cb) => {
let extension = (file.originalname.split('.').pop())
//Put here your custom validation for file extensións.
// To accept the file pass `true`, like so:
cb(null, true);
// To reject this file pass `false` or throw Exception, like so:
//cb(new HttpException ("File format is not valid", HttpStatus.BAD_REQUEST), false)
},
limits: {
fileSize: 2097152 //2 Megabytes
},
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, './public');
},
filename(req, file, cb) {
cb(null, "usman_" + file.originalname);
},
})
}),
],
providers: [UserService],
controllers: [UserController],
})
NestJs Code Component Code
#Post("upload")
#UseInterceptors(FileInterceptor('file'))
async upload(#UploadedFile() file, #Request() req) {
console.log(file);
console.log(req.files);
}
I assume you are using the FastifyAdapter instead of express.
In the documentation it says:
Multer will not process any form which is not multipart
(multipart/form-data). Besides, this package won't work with the
FastifyAdapter.
So if possible, switch to express. Otherwise, you have to use Fastify-Multipart until it is natively supported by nest.js.

Upload file using nestjs and multer

Since nestjs is an express app, it's possible to use any library to handle upload using nest, and since it provides Midlewares, it's also possible to use multer. My question is: What's the best way to handle file uploads using nestjs?
As informed by #Kamyl on issue https://github.com/nestjs/nest/issues/262, since v4.6.0 is possible to upload files using multer to nestjs using a common file interceptor.
import { ... , UseInterceptors, FileInterceptor, UploadedFile } from '#nestjs/common'
...
#UseInterceptors(FileInterceptor('file'))
async upload( #UploadedFile() file) {
console.log(file)
}
This way the variable file will have a buffer
Using Multer options
It's also needed the field name as the first param, then an array with Multer Options
import { ... , UseInterceptors, FileInterceptor, UploadedFile } from '#nestjs/common'
import { diskStorage } from 'multer'
import { extname } from 'path'
...
#UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: './uploads'
, filename: (req, file, cb) => {
// Generating a 32 random chars long string
const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('')
//Calling the callback passing the random name generated with the original extension name
cb(null, `${randomName}${extname(file.originalname)}`)
}
})
}))
async upload( #UploadedFile() file) {
console.log(file)
}
This way the variable file will have a filename, destination and path.
The destination param from the diskStorage can also be a function, with the parameters and expecting the callback the same as filename. By passing a diskStorage the file will be automatically saved to the destination informed with the filename given.
It's also possible to handle multiple files by using #UploadedFiles and FilesInterceptor (plural)
A cleaner way would be to extract the configurations to a separate file and then call it inside the interceptor method
import { extname } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { diskStorage } from 'multer';
import { v4 as uuid } from 'uuid';
import { HttpException, HttpStatus } from '#nestjs/common';
// Multer configuration
export const multerConfig = {
dest: process.env.UPLOAD_LOCATION,
};
// Multer upload options
export const multerOptions = {
// Enable file size limits
limits: {
fileSize: +process.env.MAX_FILE_SIZE,
},
// Check the mimetypes to allow for upload
fileFilter: (req: any, file: any, cb: any) => {
if (file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) {
// Allow storage of file
cb(null, true);
} else {
// Reject file
cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
}
},
// Storage properties
storage: diskStorage({
// Destination storage path details
destination: (req: any, file: any, cb: any) => {
const uploadPath = multerConfig.dest;
// Create folder if doesn't exist
if (!existsSync(uploadPath)) {
mkdirSync(uploadPath);
}
cb(null, uploadPath);
},
// File modification details
filename: (req: any, file: any, cb: any) => {
// Calling the callback passing the random name generated with the original extension name
cb(null, `${uuid()}${extname(file.originalname)}`);
},
}),
};
and then call it under the interceptor like so
import { ... , UseInterceptors, FileInterceptor, UploadedFile } from '#nestjs/common'
import { diskStorage } from 'multer'
import { extname } from 'path'
import { multerOptions } from 'src/config/multer.config';
...
#Post('/action/upload')
#UseInterceptors(FileInterceptor('file', multerOptions))
async upload( #UploadedFile() file) {
console.log(file)
}
Cleanest implementation using Multer options
Thank you #VictorIvens for the best answer out of the bunch.
However, I found the following problems in the code.
the import called FileInterceptor does not exist withing #nestjs/common package int the latest version of NestJS.
the code looks a bit too cluttered to my eyes.
So, to simplify the things up, I have come up with the following solution.
storage.config.ts
export const storage = diskStorage({
destination: "./uploads",
filename: (req, file, callback) => {
callback(null, generateFilename(file));
}
});
function generateFilename(file) {
return `${Date.now()}.${extname(file.originalname)}`;
}
your-controller.controller.ts
import {
Controller,
Post,
UseInterceptors,
UploadedFile
} from "#nestjs/common";
import { FileInterceptor } from "#nestjs/platform-express";
import { diskStorage } from "multer";
import { extname } from "path";
import { storage } from "./storage.config"
#Controller()
export class YourController {
#Post("upload") // API path
#UseInterceptors(
FileInterceptor(
"file", // name of the field being passed
{ storage }
)
)
async upload(#UploadedFile() file) {
return file;
}
}
**
2021 Update
**
To do this now, you need to import FileInterceptor like this...
import { FileInterceptor } from '#nestjs/platform-express';
If you are getting the data from the user via API call, you can save the data as buffer and access the content using adm-zip. Below is the controller method implementation in nest.js.
#Post("/blackBoardUpload")
#UseInterceptors(
FileInterceptor('image', {
storage: memoryStorage(),
fileFilter: zipFileFilter,
}),
)
async uploadedFile(#UploadedFile() file) {
console.log(file)
const response = {
originalname: file.originalname,
filename: file.filename,
};
var AdmZip = require('adm-zip');
var zip = new AdmZip(file.buffer);
var zipEntries = zip.getEntries();
console.log(zipEntries.length);
return {
status: HttpStatus.OK,
message: 'Received Zip file successfully!',
data: response,
};
}
Create a helper.ts file that rename your file and contains path
export class Helper {
static customFileName(req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
let fileExtension = "";
if(file.mimetype.indexOf("jpeg") > -1){
fileExtension = "jpg"
}else if(file.mimetype.indexOf("png") > -1){
fileExtension = "png";
}
const originalName = file.originalname.split(".")[0];
cb(null, originalName + '-' + uniqueSuffix+"."+fileExtension);
}
static destinationPath(req, file, cb) {
cb(null, 'uploads/')
}
}
code for controller
import { Helper } from '../service/Helper';
import { diskStorage } from 'multer';
import {FileInterceptor} from '#nestjs/platform-express'
import {Controller, Post, Body, UseInterceptors, UploadedFile} from '#nestjs/common'
#Post('upload')
#UseInterceptors(
FileInterceptor('picture', {
storage: diskStorage({
destination: Helper.destinationPath,
filename: Helper.customFileName,
}),
}),
)
uploadFile(#UploadedFile() file: Express.Multer.File) {
console.log(file);
}
A simple way is to use controllers. You need to define an upload controller and add it in your app.module, this is an example of what a controller should be (back-end):
#Controller()
export class Uploader {
#Post('sampleName')
#UseInterceptors(FileInterceptor('file'))
uploadFile(#UploadedFile() file) {
// file name selection
const path = `desired path`;
const writeStream = fs.createWriteStream(path);
writeStream.write(file.buffer);
writeStream.end();
return {
result: [res],
};
}
}
And call your controller by fetch in the front-end:
fetch('controller address', {
method: 'POST',
body: data,
})
.then((response) => response.json())
.then((success) => {
// What to do when succeed
});
})
.catch((error) => console.log('Error in uploading file: ', error));

Resources