TypeGraphQl: Usage with Netlify Functions/AWS Lambda - 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

Related

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

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);
};

apollo-server-lambda: Unable to determine event source based on event

I am using apollo-server-lambda for my app. I have create custom authoization http headers and it is required . if authoization: LETMEIN then it will return true and also return all data, if there is no any authoization or wrong authoization then it wll throw an error. For local development I used serverless-offline.In Local environment, it works as expected and here is the image but when I deploy my code to AWS, the api end does not work. It always throws me the error: here is the link.
I test my function AWS console. I am getting this error:
I did not get what I am doing wrong.
Here is my code
/* eslint-disable #typescript-eslint/no-var-requires */
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core';
import { ApolloServer, AuthenticationError } from 'apollo-server-lambda';
import schema from '../graphql/schema';
import resolvers from '../resolvers';
import runWarm from '../utils/run-warm';
export const authToken = (token: string) => {
if (token === 'LETMEIN') {
return;
} else {
throw new AuthenticationError('No authorization header supplied');
}
};
const server = new ApolloServer({
typeDefs: schema,
resolvers,
debug: false,
plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
context: ({ event }) => {
//console.log(context);
if (event.headers) {
authToken(event.headers.authorization);
}
},
});
export default runWarm(
server.createHandler({
expressGetMiddlewareOptions: {
cors: {
origin: '*',
credentials: true,
allowedHeaders: ['Content-Type', 'Origin', 'Accept'],
optionsSuccessStatus: 200,
maxAge: 200,
},
},
})
);
This is my Lambda function
/**
* Running warm functions help prevent cold starts
*/
const runWarm =
(lambdaFunc: AWSLambda.Handler): AWSLambda.Handler =>
(event, context, callback) => {
// Detect the keep-alive ping from CloudWatch and exit early. This keeps our
// lambda function running hot.
if (event.source === 'serverless-plugin-warmup') {
return callback(null, 'pinged');
}
return lambdaFunc(event, context, callback);
};
export default runWarm;
This is not a direct answer, but might help, and could be useful if anyone else (like me) found this thread because of the error "Unable to determine event source based on event" when using apollo-server-lambda.
That error is coming from #vendia/serverless-express which is being used by apollo-server-lambda.
Within serverless-express, in src/event-sources/utils.js, there is a function called getEventSourceNameBasedOnEvent(), which is throwing the error. It needs to find something in the event object, and after a bit of experimentation I found that writing the lambda function like this solved the issue for me:
const getHandler = (event, context) => {
const server = new ApolloServer({
typeDefs,
resolvers,
debug: true,
});
const graphqlHandler = server.createHandler();
if (!event.requestContext) {
event.requestContext = context;
}
return graphqlHandler(event, context);
}
exports.handler = getHandler;
Note that the context object is added to the event object with the key "requestContext"....that's the fix.
(Also note that I have defined typeDefs and resolvers elsewhere in the code)
I can't guarantee this is the ideal thing to do, but it did work for me.

Error when building typedefs TypeError: Cannot read property 'some' of undefined

I am getting the following error when building Typedefs in Apollo Server:
return typeDef.definitions.some(definition => definition.kind === language_1.Kind.DIRECTIVE_DEFINITION &&
^
TypeError: Cannot read property 'some' of undefined
I tried to follow some solutions from here https://github.com/apollographql/apollo-server/issues/2961 but still, I am getting the error.
This is how I am creating the schema:
fs.readdirSync(__dirname)
.filter(dir => { console.log('dir', dir); return dir.indexOf('.') < 0 })
.forEach((dir) => {
const tmp = require(path.join(__dirname, dir)).default;
resolvers = merge(resolvers, tmp.resolvers);
typeDefs.push(tmp.types);
});
const schema = new ApolloServer({
typeDefs,
resolvers,
playground: {
endpoint: '/graphql',
settings: {
'editor.theme': 'light'
}
}
});
type.js
const Book = gql`
type Book {
title: String!
author: String!
}
`;
export const types = () => [Book];
export const typeResolvers = {
};
mutation.js
const Mutation = gql`
extend type Mutation {
addBook(book: BookInput): Book
}
`;
export const mutationTypes = () => [Mutation];
export const mutationResolvers = {
Mutation: {
addBook: async (_, args, ctx) => {
return []
}
}
};
index.js
export default {
types: () => [types, queryTypes, inputTypes, mutationTypes],
resolvers: Object.assign(queryResolvers, mutationResolvers, typeResolvers),
};
Any suggestions? What could I be missing?
I just had the same issue for the past 2 hours. I realized the file were i was instantiating my apollo server was being executed before the typedefs was created.
Simple way to test for this is to make a console.log(types, queryTypes, inputTypes, mutationTypes) right before the execution of const schema = new ApolloServer({ ....
One of them is undefined. Thanks.
After spending some time making changes, I finally got a working solution.
I had to make sure that typeDefs was an array of GraphQL Documents and not a type of [Function: types]. To do that, I removed unnecessary function syntax.
For example:
I replaced this export const types = () => [Book]; with this export const types = Book;
I replaced this types: () => [types, queryTypes, inputTypes, mutationTypes] with types: [types, queryTypes, inputTypes, mutationTypes]
... and pretty much every where I had () =>
Finally, before instantiating ApolloServer, instead of pushing tmp.types to the array of types, I used concat to use all defined graphql types in the I had defined the current file 'plus' the graphql types imported in every directory
typeDefs = typeDefs.concat(tmp.types);

How to handle apollo errors in one place with react-hooks?

I'm using React with hooks + GraphQL.
My app.jsx:
import { onError } from 'apollo-link-error';
...
const httpLink = ...
const errorLink = onError(err => console.log(err))
const terminatingLink = split(...httpLink, errorLink)
const client = new ApolloClient({
link: ApolloLink.from([terminatingLink])
...
})
<ApolloProvider client={client}>
<ErrorProvider>
</ErrorProvider>
</ApolloProvider>
Error provider is used as a common state for errors, i.e. if mutation response is bad.
Currently I create onError handler for each(!) mutation and query, i.e. like this:
const [createTeam] = useMutation(createTeamQ, {
onError: (err) => { dispatchError(err) }
})
This looks like overcoding, since I do it too often.
But I can't figure out how to dispatch the error in AppolloClient only once in onError() function imported from 'apollo-link-error'. I can't use hooks there.
Should I use redux for this particular case?
Thanks in advance.

Update method in mutation not running

I have the following component that mutates data. Apollo provides functionality to update the store automatically. I would like to control the way the data is added to the store using the update function. The documentation is straightforward enough, but I can't get it working. What is wrong in the code below that would prevent the console.log from printing.
import React from 'react'
import { connect } from 'react-redux';
import { graphql, gql, compose } from 'react-apollo';
import { personCodeSelector } from '../../selectors/auth';
import UploadBankStatement from '../../components/eftFileUploads/UploadBankStatement.jsx';
const createEftFileUpload = gql`mutation createEftFileUpload(
$bankAccountCode: String!,
$uploadInput: UploadInput!,
$uploadedByPersonCode: String!) {
createEftFileUpload(
bankAccountCode: $bankAccountCode,
uploadInput: $uploadInput,
uploadedByPersonCode: $uploadedByPersonCode) {
id
bankAccountCode
fileName
numberOfProcessedItems
numberOfUnallocatedItems
createdAt
status
}
}`;
const mutationConfig = {
props: ({ ownProps, mutate }) => ({
createEftFileUpload: (bankAccountCode, uploadInput) => {
return mutate({
variables: {
bankAccountCode,
uploadInput,
uploadedByPersonCode: ownProps.personCode
},
update: (store, something) => {
console.log("ping");
console.log(store, something);
},
});
}
})
};
const mapStateToProps = state => {
return {
personCode: personCodeSelector(state)
};
};
export default compose(
connect(mapStateToProps),
graphql(createEftFileUpload, mutationConfig)
)(UploadBankStatement);
Note I have found a couple of similar issues, but it doesn't seem to shed any light on my situation.
Server restart fix my issue. Not sure why this was required with hot-reloading. The code was correct.

Resources