Nestjs on AWS Lambda (Serverless Framework) | How to access the event parameter? - node.js

I'm hosting a Nestjs application on AWS Lambda (using the Serverless Framework).
Please note that the implementation is behind AWS API Gateway.
Question: How can I access to event parameter in my Nest controller?
This is how I bootstrap the NestJS server:
import { APIGatewayProxyHandler } from 'aws-lambda';
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { Server } from 'http';
import { ExpressAdapter } from '#nestjs/platform-express';
import * as awsServerlessExpress from 'aws-serverless-express';
import * as express from 'express';
let cachedServer: Server;
const bootstrapServer = async (): Promise<Server> => {
const expressApp = express();
const adapter = new ExpressAdapter(expressApp);
const app = await NestFactory.create(AppModule, adapter);
app.enableCors();
await app.init();
return awsServerlessExpress.createServer(expressApp);
}
export const handler: APIGatewayProxyHandler = async (event, context) => {
if (!cachedServer) {
cachedServer = await bootstrapServer()
}
return awsServerlessExpress.proxy(cachedServer, event, context, 'PROMISE')
.promise;
};
Here is a function in one controller:
#Get()
getUsers(event) { // <-- HOW TO ACCESS event HERE?? This event is undefined.
return {
statusCode: 200,
body: "This function works and returns this JSON as expected."
}
I'm struggling to understand how I can access the event paramenter, which is easily accessible in a "normal" node 12.x Lambda function:
module.exports.hello = async (event) => {
return {
statusCode: 200,
body: 'In a normal Lambda, the event is easily accessible, but in NestJS its (apparently) not.'
};
};

Solution:
Add AwsExpressServerlessMiddleware to your setup during bootstrap:
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
app.use(awsServerlessExpressMiddleware.eventContext())
Note: The app.use should be before app.init()
Now the event and context object can be accessed:
var event = req.apiGateway.event;
var context = req.apiGateway.context;
Credits: This answer on SO

Related

how to prevent file upload when body validation fails in nestjs

I have the multipart form to be validated before file upload in nestjs application. the thing is that I don't want the file to be uploaded if validation of body fails.
here is how I wrote the code for.
// User controller method for create user with upload image
#Post()
#UseInterceptors(FileInterceptor('image'))
create(
#Body() userInput: CreateUserDto,
#UploadedFile(
new ParseFilePipe({
validators: [
// some validator here
]
})
) image: Express.Multer.File,
) {
return this.userService.create({ ...userInput, image: image.path });
}
Tried so many ways to turn around this issue, but didn't reach to any solution
Interceptors run before pipes do, so there's no way to make the saving of the file not happen unless you manage that yourself in your service. However, another option could be a custom exception filter that unlinks the file on error so that you don't have to worry about it post-upload
This is how I created the whole filter
import { isArray } from 'lodash';
import {
ExceptionFilter,
Catch,
ArgumentsHost,
BadRequestException,
} from '#nestjs/common';
import { Request, Response } from 'express';
import * as fs from 'fs';
#Catch(BadRequestException)
export class DeleteFileOnErrorFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const getFiles = (files: Express.Multer.File[] | unknown | undefined) => {
if (!files) return [];
if (isArray(files)) return files;
return Object.values(files);
};
const filePaths = getFiles(request.files);
for (const file of filePaths) {
fs.unlink(file.path, (err) => {
if (err) {
console.error(err);
return err;
}
});
}
response.status(status).json(exception.getResponse());
}
}

Mocking (actually, spying on) Sentry in a jest test in a Node.js project

I'm trying to mock (well really, spyOn) Sentry with jest in a Node.js project and it is not working.
The /debug-sentry route just throws an exception, and interactively it correctly sends an exception to Sentry. When I try to mock it like this, though, the expect...toHaveBeenCalled fails. Am I not mocking this right? Suggestions?
import * as Sentry from '#sentry/node';
...
describe('failure modes', () => {
it('send an exception to Sentry if exception thrown', async () => {
const mock = jest.spyOn(Sentry, 'captureException');
const res = await request(server).get('/debug-sentry').expect('Content-Type', /text/).expect(500);
expect(mock).toHaveBeenCalled(); // This does not succeed, not sure why...
...
The code under test (I just added the /debug-sentry route to my server temporarily for testing):
import express from 'express';
import { Express } from 'express-serve-static-core';
import { requestLogger } from '#/utils/request_logger';
import {
initialize_exception_reporting,
initialize_error_reporting,
send_exception_message
} from '#/utils/exception_reporting';
export async function createServer(): Promise<Express> {
const server = express();
initialize_exception_reporting(server);
server.use(requestLogger);
// error customization, if request is invalid
// eslint-disable-next-line #typescript-eslint/no-explicit-any,#typescript-eslint/no-unused-vars
server.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
res.status(err.status).json({
error: {
type: 'request_validation',
message: err.message,
errors: err.errors,
},
});
});
server.get("/debug-sentry", (req, res) => {
throw new Error("My first Sentry error!");
});
initialize_error_reporting(server);
return server;
}
and the code setting up Sentry:
import Express from 'express';
import * as Sentry from "#sentry/node";
import * as Tracing from "#sentry/tracing";
export function initialize_exception_reporting(server: Express.Express) {
Sentry.init({
integrations: [],
});
server.use(Sentry.Handlers.requestHandler());
}
export function initialize_error_reporting(server: Express.Express) {
server.use(Sentry.Handlers.errorHandler());
}
export function send_exception_message(message: string) {
Sentry.captureMessage(message);
}

Vendia Serverless Express does not pass custom event headers to express request headers

I have an Express server running on Lambda. There is a custom lambda authorizer attached to his lambda which passes auth headers via events.
My goal is to extract these request context values and attach them to the event headers of the lambda and then pass it on to the Express request headers which can then be used across my route implementations
lambda.ts
'use strict';
import { APIGatewayProxyHandler } from "aws-lambda";
import serverlessExpress from "#vendia/serverless-express";
import app from "./express.app";
import MongoDbConnectionService from "./services/mongodb-connection-service";
let connection = null;
let serverlessExpressInstance = null;
export const handler: APIGatewayProxyHandler = async (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
const authHeaders = (event.requestContext || {}).authorizer || {};
if(connection == null) {
connection = await MongoDbConnectionService.connect();
}
if(serverlessExpressInstance) {
return serverlessExpressInstance(event, context, callback);
}
['principalId'].forEach((headerKey) => {
if (authHeaders.hasOwnProperty(headerKey)) {
event.headers[headerKey.toLowerCase()] = authHeaders[headerKey];
}
});
console.log("Event headers: ", event.headers);
serverlessExpressInstance = serverlessExpress({ app });
return serverlessExpressInstance(event, context, callback);
};
As you can see that I am extracting "principalId" from the event.requestContext and adding it to the event.headers.
When I log the event.headers it does show that "principalid" is included in it.
express server
'use strict';
import cors from 'cors';
import express from 'express';
import 'reflect-metadata';
import { ExpressRouter } from './routes/routes';
import path from 'path';
class ExpressApplication {
public app: express.Express;
private readonly router: express.Router;
constructor() {
const expressRouter = new ExpressRouter();
this.app = express();
this.router = express.Router();
this.app.use(cors());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
expressRouter.setRoutes(this.app, this.router);
}
}
const app = new ExpressApplication();
export default app.app;
route
app.post("/calendar-integration", requestValidator(CalIntegrationValidationModel), (req: Request, res: Response) => {
console.log(req.headers);
res.send("Testing this shit");
});
In the above console.log(req.headers) the principalid header is missing. I am unable to figure out what I might be doing wrong, this has been working for me when I was using aws-serverless-express before it got depreciated in favor of #vendia/serverless-express.
Seems like we need to pass the custom headers to event headers via multiValueHeaders.
So the code should be
['principalId'].forEach((headerKey) => {
if (authHeaders.hasOwnProperty(headerKey)) {
event.multiValueHeaders[headerKey.toLowerCase()] = authHeaders[headerKey];
}
});

TypeError: metadata_1.Public is not a function (NestJS SetMetaData)

My e2e test is returning TypeError: metadata_1.Public is not a function for a controller that is using the custom decorator #Public()
Some code is omitted for clarity
it(`/GET forks`, async () => {
const fork: ForksModel = {
type: 'Full Copy',
};
await request(app.getHttpServer())
.get('/forks')
.expect(200)
.expect({ fork: expectedForks});
});
#Public()
public async getAccountForks(#Req() req: Request) {
const { account } = req;
const fork = await this.service.getAccountForks(account);
return { fork, account };
}
public.decorator.ts
import { SetMetadata } from "#nestjs/common";
export const Public = () => SetMetadata( "isPublic", true );
I don't know what is happening here, it doesn't complain this when running nest
This is imported
import { Public } from '#app/utils/metadata';
So i just forgot to export my metadata files from the root utils index.ts!
But Nest didn't complain and the decorator was functional on my Guard when testing!

socket io on sails js as API and node+react as Frontend

I have an API build using sailsjs and a react redux attach to a nodejs backend, and i am trying to implement socket.io for a realtime communication, how does this work?
is it
socket.io client on the react side that connects to a socket.io server on its nodejs backend that connects to a socket.io server on the API
socket.io client on the react side and on its nodejs backend that connects to a socket.io server on the API
i have tried looking around for some answers, but none seems to meet my requirements.
to try things out, i put the hello endpoint on my API, using the sailsjs realtime documentation, but when i do a sails lift i got this error Could not fetch session, since connecting socket has no cookie (is this a cross-origin socket?) i figure that i need to pass an auth code inside the request headers Authorization property.
Assuming i went for my #1 question, and by using redux-socket.io,
In my redux middleware i created a socketMiddleware
import createSocketIoMiddleware from 'redux-socket.io'
import io from 'socket.io-client'
import config from '../../../config'
const socket = io(config.host)
export default function socketMiddleware() {
return createSocketIoMiddleware(
socket,
() => next => (action) => {
const { nextAction, shuttle, ...rest } = action
if (!shuttle) {
return next(action)
}
const { socket_url: shuttleUrl = '' } = config
const apiParams = {
data: shuttle,
shuttleUrl,
}
const nextParams = {
...rest,
promise: api => api.post(apiParams),
nextAction,
}
return next(nextParams)
},
)
}
and in my redux store
import { createStore, applyMiddleware, compose } from 'redux'
import createSocketIoMiddleware from 'redux-socket.io'
...
import rootReducers from '../reducer'
import socketMiddleware from '../middleware/socketMiddleware'
import promiseMiddleware from '../middleware/promiseMiddleware'
...
import config from '../../../config'
export default function configStore(initialState) {
const socket = socketMiddleware()
...
const promise = promiseMiddleware(new ApiCall())
const middleware = [
applyMiddleware(socket),
...
applyMiddleware(promise),
]
if (config.env !== 'production') {
middleware.push(DevTools.instrument())
}
const createStoreWithMiddleware = compose(...middleware)
const store = createStoreWithMiddleware(createStore)(rootReducers, initialState)
...
return store
}
in my promiseMiddleware
export default function promiseMiddleware(api) {
return () => next => (action) => {
const { nextAction, promise, type, ...rest } = action
if (!promise) {
return next(action)
}
const [REQUEST, SUCCESS, FAILURE] = type
next({ ...rest, type: REQUEST })
function success(res) {
next({ ...rest, payload: res, type: SUCCESS })
if (nextAction) {
nextAction(res)
}
}
function error(err) {
next({ ...rest, payload: err, type: FAILURE })
if (nextAction) {
nextAction({}, err)
}
}
return promise(api)
.then(success, error)
.catch((err) => {
console.error('ERROR ON THE MIDDLEWARE: ', REQUEST, err) // eslint-disable-line no-console
next({ ...rest, payload: err, type: FAILURE })
})
}
}
my ApiCall
/* eslint-disable camelcase */
import superagent from 'superagent'
...
const methods = ['get', 'post', 'put', 'patch', 'del']
export default class ApiCall {
constructor() {
methods.forEach(method =>
this[method] = ({ params, data, shuttleUrl, savePath, mediaType, files } = {}) =>
new Promise((resolve, reject) => {
const request = superagent[method](shuttleUrl)
if (params) {
request.query(params)
}
...
if (data) {
request.send(data)
}
request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body))
},
))
}
}
All this relation between the middlewares and the store works well on regular http api call. My question is, am i on the right path? if i am, then what should i write on this reactjs server part to communicate with the api socket? should i also use socket.io-client?
You need to add sails.io.js at your node server. Sails socket behavior it's quite tricky. Since, it's not using on method to listen the event.
Create sails endpoint which handle socket request. The documentation is here. The documentation is such a pain in the ass, but please bear with it.
On your node server. You can use it like
import socketIOClient from 'socket.io-client'
import sailsIOClient from 'sails.io.js'
const ioClient = sailsIOClient(socketIOClient)
ioClient.sails.url = "YOUR SOCKET SERVER URL"
ioClient.socket.get("SAILS ENDPOINT WHICH HANDLE SOCKET", function(data) {
console.log('Socket Data', data);
})

Resources