5

I am trying to upload multiple files with nestjs using the fastify adapter. I can do so following the tutorial in this link -article on upload

Now this does the job of file upload using fastify-multipart, but I couldnt make use of the request validations before uploading, for example, here is my rule-file-models (which later I wanted to save to postgre)

    import {IsUUID, Length, IsEnum, IsString, Matches, IsOptional} from "class-validator";
    import { FileExtEnum } from "./enums/file-ext.enum";
    import { Updatable } from "./updatable.model";
    import {Expose, Type} from "class-transformer";
    
    export class RuleFile {
      @Expose()
      @IsUUID("4", { always: true })
      id: string;
    
      @Expose()
      @Length(2, 50, {
        always: true,
        each: true,
        context: {
          errorCode: "REQ-000",
          message: `Filename shouldbe within 2 and can reach a max of 50 characters`,
        },
      })
      fileNames: string[];
    
      @Expose()
      @IsEnum(FileExtEnum, { always: true, each: true })
      fileExts: string[];
    
      @IsOptional({each: true, message: 'File is corrupated'})
      @Type(() => Buffer)
      file: Buffer;
    }
    
    export class RuleFileDetail extends RuleFile implements Updatable {
      @IsString()
      @Matches(/[aA]{1}[\w]{6}/)
      recUpdUser: string;
    }

And I wanted to validate the multipart request and see if these are set properly. I cannot make it to work with event subscription based approach. Here are a few things I tried - adding the interceptor, to check for the request

    @Injectable()
    export class FileUploadValidationInterceptor implements NestInterceptor {
      intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    
        const req: FastifyRequest = context.switchToHttp().getRequest();
        console.log('inside interceptor', req.body);
        // content type cmes with multipart/form-data;boundary----. we dont need to valdidate the boundary
        // TODO: handle split errors based on semicolon
        const contentType = req.headers['content-type'].split(APP_CONSTANTS.CHAR.SEMI_COLON)[0];
    
        console.log(APP_CONSTANTS.REGEX.MULTIPART_CONTENT_TYPE.test(contentType));
        const isHeaderMultipart = contentType != null?
            this.headerValidation(contentType): this.throwError(contentType);
        
      **// CANNOT check fir req.file() inside this, as it throws undefined**
        return next.handle();
      }
    
      headerValidation(contentType) {
        return APP_CONSTANTS.REGEX.MULTIPART_CONTENT_TYPE.test(contentType) ? true : this.throwError(contentType);
      }
      throwError(contentType: string) {
        throw AppConfigService.getCustomError('FID-HEADERS', `Request header does not contain multipart type: 
        Provided incorrect type - ${contentType}`);
      }
    }

I wasnt able to check req.file() in the above interceptor. It throws as undefined. I tried to follow the fastify-multipart

But I wasnt able to get the request data in a prehandler as provided in the documentation for fastify-multipart

    fastify.post('/', async function (req, reply) {
      // process a single file
      // also, consider that if you allow to upload multiple files
      // you must consume all files othwise the promise will never fulfill
      const data = await req.file()
     
      data.file // stream
      data.fields // other parsed parts
      data.fieldname
      data.filename
      data.encoding
      data.mimetype
     
      // to accumulate the file in memory! Be careful!
      //
      // await data.toBuffer() // Buffer
      //
      // or
     
      await pump(data.file, fs.createWriteStream(data.filename))

I tried getting via by registering a prehandler hook of my own like this (executed as iife)

    (async function bootstrap() {
      const appConfig = AppConfigService.getAppCommonConfig();
      const fastifyInstance = SERVERADAPTERINSTANCE.configureFastifyServer();
      // @ts-ignore
      const fastifyAdapter = new FastifyAdapter(fastifyInstance);
      app = await NestFactory.create<NestFastifyApplication>(
        AppModule,
        fastifyAdapter
      ).catch((err) => {
        console.log("err in creating adapter", err);
        process.exit(1);
      });
    
      .....
      app.useGlobalPipes(
        new ValidationPipe({
          errorHttpStatusCode: 500,
          transform: true,
          validationError: {
            target: true,
            value: true,
          },
          exceptionFactory: (errors: ValidationError[]) => {
            // send it to the global exception filter\
            AppConfigService.validationExceptionFactory(errors);
          },
        }),
    
      );
      
      app.register(require('fastify-multipart'), {
        limits: {
          fieldNameSize: 100, // Max field name size in bytes
          fieldSize: 1000000, // Max field value size in bytes
          fields: 10, // Max number of non-file fields
          fileSize: 100000000000, // For multipart forms, the max file size
          files: 3, // Max number of file fields
          headerPairs: 2000, // Max number of header key=>value pairs
        },
      });
    
      
    
      (app.getHttpAdapter().getInstance() as FastifyInstance).addHook('onRoute', (routeOptions) => {
        console.log('all urls:', routeOptions.url);
    
        if(routeOptions.url.includes('upload')) {

    // The registration actually works, but I cant use the req.file() in the prehandler
          console.log('###########################');
          app.getHttpAdapter().getInstance().addHook('preHandler', FilePrehandlerService.fileHandler);
        }
    
      });
    
      SERVERADAPTERINSTANCE.configureSecurity(app);
    
      //Connect to database
      await SERVERADAPTERINSTANCE.configureDbConn(app);
    
      app.useStaticAssets({
        root: join(__dirname, "..", "public"),
        prefix: "/public/",
      });
      app.setViewEngine({
        engine: {
          handlebars: require("handlebars"),
        },
        templates: join(__dirname, "..", "views"),
      });
    
      await app.listen(appConfig.port, appConfig.host, () => {
        console.log(`Server listening on port - ${appConfig.port}`);
      });
    })();

Here is the prehandler,

    export class FilePrehandlerService {
      constructor() {}
    
      static fileHandler = async (req, reply) => {
          console.log('coming inside prehandler');
    
              console.log('req is a multipart req',await req.file);
              const data = await req.file();
              console.log('data received -filename:', data.filename);
              console.log('data received- fieldname:', data.fieldname);
              console.log('data received- fields:', data.fields);
    
          return;
      };
    }

This pattern of registring and gettin the file using preHandler works in bare fastify application. I tried it

Bare fastify server:

    export class FileController {
        constructor() {}
    
        async testHandler(req: FastifyRequest, reply: FastifyReply) {
            reply.send('test reading dne');
        }
    
        async fileReadHandler(req, reply: FastifyReply) {
            const data = await req.file();
    
            console.log('field val:', data.fields);
            console.log('field filename:', data.filename);
            console.log('field fieldname:', data.fieldname);
            reply.send('done');
        }
    }
    
    export const FILE_CONTROLLER_INSTANCE = new FileController();

This is my route file

    const testRoute: RouteOptions<Server, IncomingMessage, ServerResponse, RouteGenericInterface, unknown> = {
        method: 'GET',
        url: '/test',
        handler: TESTCONTROLLER_INSTANCE.testMethodRouteHandler,
    };
    
    const fileRoute: RouteOptions = {
        method: 'GET',
        url: '/fileTest',
        preHandler: fileInterceptor,
        handler: FILE_CONTROLLER_INSTANCE.testHandler,
    };
    
    const fileUploadRoute: RouteOptions = {
        method: 'POST',
        url: '/fileUpload',
        preHandler: fileInterceptor,
        handler: FILE_CONTROLLER_INSTANCE.fileReadHandler,
    };
    
    const apiRoutes = [testRoute, fileRoute, fileUploadRoute];
    export default apiRoutes;

Could someone let me know the right the way to get the fieldnames , validate them befr the service being called in Nestjs

Jboucly
  • 720
  • 6
  • 17
vijayakumarpsg587
  • 1,079
  • 3
  • 22
  • 39
  • i need to use fastify with nestjs to post fields(not files) multipart data. In both my application and your github link, req.body is undefined. I posted my question here stackoverflow.com/questions/69898572/… can you pls help? @vijayakumarpsg587 – Raj Nov 12 '21 at 10:44

2 Answers2

22

Well, I have done something like this and It works great for me. Maybe it can work for you too.

// main.ts
import multipart from "fastify-multipart";

const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
);
app.register(multipart);
// upload.guard.ts
import {
    Injectable,
    CanActivate,
    ExecutionContext,
    BadRequestException,
} from "@nestjs/common";
import { FastifyRequest } from "fastify";

@Injectable()
export class UploadGuard implements CanActivate {
    public async canActivate(ctx: ExecutionContext): Promise<boolean> {
        const req = ctx.switchToHttp().getRequest() as FastifyRequest;
        const isMultipart = req.isMultipart();
        if (!isMultipart)
            throw new BadRequestException("multipart/form-data expected.");
        const file = await req.file();
        if (!file) throw new BadRequestException("file expected");
        req.incomingFile = file;
        return true;
    }
}
// file.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import { FastifyRequest } from "fastify";

export const File = createParamDecorator(
    (_data: unknown, ctx: ExecutionContext) => {
        const req = ctx.switchToHttp().getRequest() as FastifyRequest;
        const file = req.incomingFile;
        return file
    },
);
// post controller
@Post("upload")
@UseGuards(UploadGuard)
uploadFile(@File() file: Storage.MultipartFile) {
    console.log(file); // logs MultipartFile from "fastify-multipart"
    return "File uploaded"
}

and finally my typing file

declare global {
    namespace Storage {
        interface MultipartFile {
            toBuffer: () => Promise<Buffer>;
            file: NodeJS.ReadableStream;
            filepath: string;
            fieldname: string;
            filename: string;
            encoding: string;
            mimetype: string;
            fields: import("fastify-multipart").MultipartFields;
        }
    }
}

declare module "fastify" {
    interface FastifyRequest {
        incomingFile: Storage.MultipartFile;
    }
}
Jboucly
  • 720
  • 6
  • 17
barbarbar338
  • 616
  • 5
  • 15
  • THank you so much. Let me try this. The code looks good though – vijayakumarpsg587 Feb 24 '21 at 03:15
  • Thank you ! I'm using GraphQL, and I've the same issue. Do you know how to make it work with GraphQL? Related issue and small example of code : https://github.com/nestjs/docs.nestjs.com/issues/103#issuecomment-787005428 – Sir Mishaa Feb 27 '21 at 16:20
  • Sorry, I've never used graphql with nestjs :/ – barbarbar338 Feb 27 '21 at 19:59
  • @SirMishaa : I understand that it might not be the right one, but were u able to make sure that first "multipart/form-data" media type is supported. There was one similar issue reported (although it might not be the case with you). Can u please check - https://stackoverflow.com/questions/64006287/getting-unsupported-media-type-in-postman-response-while-sending-graphql-api-moc – vijayakumarpsg587 Nov 13 '21 at 13:08
  • tried with `@fastify/multipart` and am getting `upload.guard.ts - error TS2741: Property 'filepath' is missing in type 'import("./node_modules/@fastify/multipart/index").MultipartFile' but required in type 'Storage.MultipartFile'` – Drew Jan 23 '23 at 20:37
1

So I found a simpler alternative. I started using fastify-multer. I used it along with this awesome lib - which made me use the multer for fastify - @webundsoehne/nest-fastify-file-upload

These are the changes I made. I registered the multer content process.

app.register(multer( {dest:path.join(process.cwd()+'/upload'),
        limits:{
            fields: 5, //Number of non-file fields allowed
            files: 1,
            fileSize: 2097152,// 2 MB,
        }}).contentParser);

Then in the controller - I use it as the nestjs doc says . This actually makes fasitfy work with multer

@UseInterceptors(FileUploadValidationInterceptor, FileInterceptor('file'))
      @Post('/multerSample')
      async multerUploadFiles(@UploadedFile() file, @Body() ruleFileCreate: RuleFileCreate) {
        console.log('data sent', ruleFileCreate);
        console.log(file);
        // getting the original name of the file - no matter what
        ruleFileCreate.originalName = file.originalname;
        return await this.fileService.fileUpload(file.buffer, ruleFileCreate);
}

BONUS - storing the file in local and storing it in DB - Please refer

github link

Pini Cheyni
  • 5,073
  • 2
  • 40
  • 58
vijayakumarpsg587
  • 1,079
  • 3
  • 22
  • 39
  • i need to use fastify with nestjs to post fields(not files) multipart data. In both my application and your github link, req.body is undefined. I posted my question here https://stackoverflow.com/questions/69898572/multipart-form-data-body-is-empty-using-nestjs-and-fastify can you pls help? @vijayakumarpsg587 – Raj Nov 12 '21 at 10:36
  • @Raj were u able to refer to the github link ? Any luck with that? – vijayakumarpsg587 Nov 13 '21 at 13:09