How to use enhancers (pipes, guards, interceptors, etc) with Nestjs Standalone app - aws-lambda

The Nestjs module system is great, but I'm struggling to figure out how to take full advantage of it in a Serverless setting.
I like the approach of writing my domain logic in *.service.ts files, while using *.controller.ts files to take care of non-business related tasks such as validating an HTTP request body and converting to a DTO before invoking methods in a service.
I found the section on Serverless in the nestjs docs and determined that for my specific use-case, I need to use the "standalone application feature".
I created a sample nestjs app here to illustrate my problem.
The sample app has a simple add() function to add two numbers. I use class-validator for validation on the AddDto class.
// add.dto.ts
import { IsNumber } from 'class-validator'
export class AddDto {
#IsNumber()
public a: number;
#IsNumber()
public b: number;
}
And then, via some Nestjs magic, I am able to get built-in validation using the AddDto inside my controller by doing the following:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Use `ValidationPipe()` for auto-validation in controllers
app.useGlobalPipes(
new ValidationPipe({ transform: true })
)
await app.listen(3000);
}
// app.controller.ts
#Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
#Post('add')
add(#Body() dto: AddDto): number {
// Request body gets auto validated and converted
// to an instance of `AddDto`, sweet!
return this.appService.add(dto.a, dto.b);
}
}
// app.service.ts
#Injectable()
export class AppService {
add(a: number, b: number): number {
return a + b
}
}
So far, so good. The problem now arises when using this in AWS with a Lambda function, namely:
I want to re-use the business logic in app.service.ts
I want to re-use built in validation that happens when making an HTTP request to the app, such as in the example above.
I want to use the standalone app feature so I don't have to spin up an entire nest server in Lambda
The docs hint on this being a problem:
Be aware that NestFactory.createApplicationContext does not wrap controller methods with enhancers (guard, interceptors, etc.). For this, you must use the NestFactory.create method.
For example, I have a lambda that receives messages from AWS EventBridge. Here's a snippet from the sample app:
// standalone-app.ts
interface IAddCommand {
a: number;
b: number;
}
export const handler = async (
event: EventBridgeEvent<'AddCommand', IAddCommand>,
context: any
) => {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
const { a, b } = event.detail;
const sum = appService.add(a, b)
// do work on `sum`, like cache the result, etc...
return sum
};
// lambda-handler.js
const { handler } = require('./dist/standalone-app')
handler({
detail: {
a: "1", // is a string, should be a number
b: "2" // is a string, should be a number
}
})
.then(console.log) // <--- prints out "12" ("1" + "2") instead of "3" (1 + 2)
I don't get "free" validation of the event's payload in event.detail like I do with #Body() dto: AddDto when making a HTTP POST request to /add. Preferentially, the code would throw a validation error in the above example. Instead, I get an answer of "12" -- a false positive.
Hopefully, this illustrates the crux of my problem. I still want to validate the payload of the event before calling appService.add(a, b), but I don't want to write custom validation logic that already exists on the controller in app.controller.ts.
Ideas? Anyone else run into this before?

It occurred to me while writing this behemoth of a question that I can simply use class-validator and class-transformer in my Lambda handler.
import { validateOrReject } from 'class-validator'
import { plainToClass } from 'class-transformer'
import { AddDto } from 'src/dto/add.dto'
export const handler = async (event: any, context: any) => {
const appContext = await NestFactory.createApplicationContext(AppModule);
const appService = appContext.get(AppService);
const data = getPayloadFromEvent(event)
// Convert raw data to a DTO
const dto: AddDto = plainToClass(AddDto, data)
// Validate it!
await validateOrReject(dto)
const sum = appService.add(dto.a, dto.b)
// do work on `sum`...
}
It's not as "free" as using app.useGlobalPipes(new ValidationPipe()), but only involves a few extra lines of code.

It worked for me with the following lambda file for nestjs.
import { configure as serverlessExpress } from '#vendia/serverless-express';
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '#nestjs/common';
let cachedServer;
export const handler = async (event, context) => {
if (!cachedServer) {
const nestApp = await NestFactory.create(AppModule);
await nestApp.useGlobalPipes(new ValidationPipe());
await nestApp.init();
cachedServer = serverlessExpress({
app: nestApp.getHttpAdapter().getInstance(),
});
}
return cachedServer(event, context);
};

Related

React Query queryClient.setQueryData isn't updating the cached data

I have a custom hook that looks something like this:
import { useQuery, useQueryClient } from 'react-query'
import { get } from '#/util/api' // Custom API utility
import produce from 'immer' // Using immer for deep object mutation
export function useData() {
const queryClient = useQueryClient()
const { data, isSuccess } = useQuery(
'myData', () => get('data')
)
function addData(moreData) {
const updatedData = produce(data.results, (draft) => {
draft.push(moreData)
})
setData(updatedData)
}
function setData(newData) {
queryClient.setQueryData('myData', newData)
}
return {
data: data && data.results,
setData,
addData,
}
}
My data in data.results is an array of objects. When I call addData it creates a copy of my current data, mutates it, then calls setData where queryClient.setQueryData is called with a new array of objects passed in as my second argument. But my cached data either doesn't update or becomes undefined in the component hooked up to the useData() hook. Does anyone know what I'm doing wrong?
code looks good from react-query perspective, but I'm not sure if that's how immer works. I think with your code, you will get back the same data instance with just a new data.results object on it. I would do:
const updatedData = produce(data, (draft) => {
draft.results.push(moreData)
})

Consuming lambda SNS event as type Promise<APIGatewayProxyResultV2>

I don't understand what is the use case to use Promise<APIGatewayProxyResultV2> type when processing an SNS event.
export async function main (event: SNSEvent) {
event.Records.forEach((record) => {
console.log('This is the record', record);
});
return {
statusCode: 200
}
}
export async function main (event: SNSEvent): Promise<APIGatewayProxyResultV2> {
event.Records.forEach((record) => {
console.log('This is the record', record);
});
return {
statusCode: 200
}
}
What is the benefit of using Promise<APIGatewayProxyResultV2> ?
Does it mean that I could define the event structure and not have it accepted, basically an if-else statement, but transformed in Web Logic? Can you point to an example if this is the case?
First question - do you have an API Gateway in use with your SNS?
The code you mentioned:
export async function main (event: SNSEvent): Promise<APIGatewayProxyResultV2>
just promises an APIGatewayProxyResult when returning. So it expects at least a statusCode and a body:
return {
statusCode:202,
body:"JSON"
};
I think, what you mean is: export async function main (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2>
I assume that you're using an API Gateway that triggers a SNS Topic that triggers a lambda. With APIGatewayProxyEventV2 you just get the whole payload from the ApiGateway. You can create an interface that extends this APIGatewayProxyResultV2 to define or rather extend the event's structure. In my case, I added my own queryStringParameters.
interface ExampleApiProxyEvent extends APIGatewayProxyEventV2 {
queryStringParameters: {
par1: string;
par2: string;
par3: string;
};
}

TypeGraphQl: Usage with Netlify Functions/AWS Lambda

I was finally able to get TypeQL working with Netlify Functions / AWS Lambda after a day of work, going over the docs and examples, and in the end desperate brute force.
I'm sharing my working code here for others (or for future reference of my own :P ) as it contains some counterintuitive keyword usage.
Normal Approach
The error I kept getting when using the simple example was:
Your function response must have a numerical statusCode. You gave: $ undefined
I searched of course in the issues, but none of the suggested solutions worked for me.
Working Code
import 'reflect-metadata'
import { buildSchema } from 'type-graphql'
import { ApolloServer } from 'apollo-server-lambda'
import { RecipeResolver } from 'recipe-resolver'
async function lambdaFunction() {
const schema = await buildSchema({
resolvers: [RecipeResolver],
})
const server = new ApolloServer({
schema,
playground: true,
})
// !!! NOTE: return (await ) server.createHandler() won't work !
exports.handler = server.createHandler()
}
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!! NOTE: weird but only way to make it work with
// AWS lambda and netlify functions (netlify dev)
// also needs a reload of the page (first load of playground won't work)
lambdaFunction()
// exports.handler = lambdaFunction wont work
// export { lambdaFunction as handler } wont work
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Also I got some reflection errors from the simple example
Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for argument named 'title' of 'recipe' of 'RecipeResolver
So I had to figure out how to add explicit type to #Arg:
// previous:
// recipe(#Arg('title') title: string)
// fixed:
recipe( #Arg('title', (type) => String) title: string
I share the code that works for me
// File: graphql.ts
import 'reflect-metadata'
import { buildSchema } from 'type-graphql'
import { ApolloServer } from 'apollo-server-lambda'
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'
import { RecipeResolver } from './recipe-resolver'
export const createHandler = async function(){
const schema = await buildSchema({
resolvers: [RecipeResolver],
})
const server = new ApolloServer({
schema,
introspection: true,
plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
})
return server.createHandler()
}
export const handler = async function(event, context, callback) {
const graphqlHandler = await createHandler()
return await graphqlHandler(event, context, callback)
}
// Lambda: graphql.handler
node16.x
type-graphql ^1.1.1
graphql ^15.3.0
apollo-server-lambda: ^3.10.2

Acceptance testing with memory datasource doesn't work

I created acceptance test of controller that using memory datasource.
Before the test start, I was trying to have clean database and only add 1 user credential to login by calling endpoint since the rest of test require authenticated access with access token.
I can get the user info in getUser after created the user record from givenUser.
However, the login request can't find the credential. It seems that the helpers and client are not sharing the same memory datasource.
I am not sure what configuration setup is wrong in this case.
src/tests/fixtures/datasources/testdb.datasource.ts
import {juggler} from '#loopback/repository';
export const testdb: juggler.DataSource = new juggler.DataSource({
name: 'db',
connector: 'memory',
});
src/datasources/mongodb.datasource.ts
import {inject} from '#loopback/core';
import {juggler} from '#loopback/repository';
import * as config from './mongodb.datasource.json';
export class MongodbDataSource extends juggler.DataSource {
static dataSourceName = 'mongodb';
constructor(
#inject('datasources.config.mongodb', {optional: true})
dsConfig: object = config,
) {
super(dsConfig);
}
}
user.controller.acceptance.ts
import {Client, expect, supertest} from '#loopback/testlab';
import {ApiApplication} from '../..';
import {setupApplication} from './test-helper';
import {givenEmptyDatabase, givenUser,getUser} from '../helpers/database.helpers';
describe('UserController', () => {
let app: ApiApplication;
let client: supertest.SuperTest<supertest.Test>;;
let jwtToken: string;
before('setupApplication', async () => {
({app, client} = await setupApplication());
});
before(givenEmptyDatabase);
before(givenUser);
before(getUser);
before(async () => {
const response = await client
.post('/login')
.send({username: 'user1', password: 'password'});
jwtToken = response.body.token;
});
after(async () => {
await app.stop();
});
it('invokes GET /info without authentication', async () => {
const expectedError = {
error: {
statusCode: 401,
name: 'UnauthorizedError',
message: 'Unauthorized'
}
};
const res = await client.get('/user/info').expect(401);
expect(res.body).to.containEql(expectedError);
});
test-help.ts
import {ApiApplication} from '../..';
import {
createRestAppClient,
givenHttpServerConfig,
Client,
} from '#loopback/testlab';
import {testdb} from '../fixtures/datasources/testdb.datasource';
export async function setupApplication(): Promise<AppWithClient> {
const app = new ApiApplication();
await app.bind('datasources.config.mongodb').to({
name: 'mongodb',
connector: 'memory',
debug: true,
});
await app.boot();
await app.start();
const client = createRestAppClient(app);
return {app, client};
}
export interface AppWithClient {
app: ApiApplication;
client: Client;
}
database.helper.ts
import { UserRepository } from '../../repositories';
import { User } from '../../models';
import { testdb } from '../fixtures/datasources/testdb.datasource';
export async function givenEmptyDatabase() {
let userRepo: UserRepository;
userRepo = new UserRepository(testdb);
userRepo.deleteAll();
}
export async function givenUser() {
let userRepo = new UserRepository(testdb);
const user = {
username: 'user1',
password: 'password',
created_at: new Date('2019-08-08'),
updated_at: new Date('2019-08-08'),
}
await userRepo.create(operator);
}
export async function getUser() {
let userRepo = new UserRepository(testdb);
const users = await userRepo.find();
console.log(users);
}
It seems that the helpers and client are not sharing the same memory datasource.
Exactly.
It's important to realize that you can have multiple datasources using the same connector, for example you can one logDb using one MongoDB server and userDb connecting to another one. The same applies to memory datasources, you can have multiple datasources using the memory connector and each instance will have its own space for data.
Since you are already changing the configuration of your main mongodb datasource in the tests, my recommendation is to use that modified datasource when setting up the initial test data.
In database.helper.ts:
export async function givenEmptyDatabase(db) {
let userRepo: UserRepository;
userRepo = new UserRepository(db);
userRepo.deleteAll();
}
export async function givenUser(db) {
// ...
}
// and so on
In your test:
before(async () => {
const db = await app.get<juggler.DataSource>('datasources.config.mongodb');
await givenEmptyDatabase(db);
});
Personally, I strongly recommend use the same database in your tests as you will use in production. The memory database behaves differently from MongoDB. Certain operations may pass when storing data in memory but fail when MongoDB is used.
You can learn more about testing in LoopBack's "Best Practice" guides: Testing your application

Preloader not shown one or more http service call simultaneously using httpInterceptors?

I want to implement the global pre-loader concept in angular using httpInterceptors, its not working suppose if two http service call simultaneously called, 'finalize' will trigger only after one or more http api calls end, but its not happened, the preloader is also not shown correctly, it hide after first api call finished. Please suggest what i missed and tell me how to handle it. Is this the right place to handle the global error and preloader concept?
app.component.html
<preloader [loading]="appService.loading"></preloader>
app.component.ts:
const url1 = "https://jsonplaceholder.typicode.com/albums/1";
const url2 = "https://jsonplaceholder.typicode.com/albums/2";
forkJoin(
this.http.get(url1),
this.http.get(url2),
).subscribe(console.log);
HttpserviceInterceptor:
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
import { AppCommonService } from './app.common.service';
#Injectable()
export class HttpserviceInterceptor implements HttpInterceptor {
constructor(
private appService: AppCommonService,
private notification: NotificationsWrapperService,
) {}
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.appService.showPreLoader()
return next.handle(request).pipe(
//tslint:disable-next-line: no-empty
tap(() => {
}, (error: any) => {
this.notification.error(error);
}),
finalize(() => this.appService.hidePreLoader()),
);
}
}
AppCommonService:
public showPreloader(): void {
//this.showPreloader$.next(true);
this.loading = true;
}
public hidePreLoader(): void {
//this.showPreloader$.next(false);
this.loading = false;
}
Lets suppose that you perform two requests. The first one takes one second to complete and the second one takes 5 seconds. Based on the code you provided the flow of the loader will be the following.
First request is performed and this.loading is set to true
Second request is performed and this.loading is set to true again
First requests finishes, and this.loading is set to false (wrong)
Second requests finishes, and this.loading is set to false
To make the loader appear as long as a request is active, you should try to keep the number of the requests that are currently performed by the web browser. Lets assume that you initialize a private integer named currentNumberOfRequests, and set it's value to 0.
So when a requests is performed, you should always set the {this.loading} flag to true and increase the this.currentNumberOfRequests by 1, and when a requests succeeds or fails (ideally in the finally clause), you should decrease the this.currentNumberOfRequests by 1. Now, if the this.currentNumberOfRequests is 0 you should hide the loader.

Resources