Nest js throttler with GraphQL is not working as expected - graphql

#Injectable()
export class GqlThrottlerGuard extends ThrottlerGuard {
getRequestResponse(context: ExecutionContext) {
const gqlCtx = GqlExecutionContext.create(context);
const ctx = gqlCtx.getContext();
return { req: ctx.req, res: ctx.res };
}
}
Here I am getting only req from ctx, res is undefined. I am using GraphQL with express in nestjs.
This is how my context object looks like before creating a GqlExecutionContext:
ExecutionContextHost {
args: [
undefined,
{ id: 2 },
{ req: [IncomingMessage] },
{
fieldName: 'user',
fieldNodes: [Array],
returnType: User!,
parentType: Query,
path: [Object],
schema: [GraphQLSchema],
fragments: [Object: null prototype] {},
rootValue: undefined,
operation: [Object],
variableValues: {},
cacheControl: [Object]
}
],
constructorRef: [class UsersResolver],
handler: [Function: findOne],
contextType: 'graphql'
}
Error:
[Nest] 30284 - 08/01/2022, 12:54:25 am ERROR [ExceptionsHandler] Cannot read property 'ip' of undefined
TypeError: Cannot read property 'ip' of undefined
at ThrottlerGuard.getTracker (C:\Users\Mahesh\OneDrive\Desktop\graphql-server\node_modules\#nestjs\throttler\dist\throttler.guard.js:95:16)

In my case, I got the following error message
cannot read property 'header' of undefined
I think the main problem comes from the ctx.res which is undefined.
To resolved the problem I used the following code:
ctx.req.res
In your case it should be:
#Injectable()
export class GqlThrottlerGuard extends ThrottlerGuard {
getRequestResponse(context: ExecutionContext) {
const gqlCtx = GqlExecutionContext.create(context);
const ctx = gqlCtx.getContext();
return { req: ctx.req, res: ctx.req.res };
}
}

I have added the req, res to the context in GraphQLModule.forRoot() So after creating Graphql execution context I am able to get the required data in the return statement { req: ctx.req, res: ctx.res }.
GraphQLModule.forRoot({
autoSchemaFile: 'src/graphql-schema.gql',
context: ({ req, res }) => ({ req, res }),
}),

Related

Apollo Server Context Request Property Does Not Exist

Here's my Apollo Server definition.
const server = new ApolloServer({
schema,
context: (async ({ req }) => {
console.log(req);
return {};
}),
csrfPrevention: true,
cache: 'bounded',
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
],
});
As we can see that auth property does exists in the req object but somehow when I tried to get the value, an error thrown Property 'auth' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>' How can I retrieve the auth property?
<ref *2> IncomingMessage {
...,
auth: { sub: '1234567890', name: 'John Doe', iat: 1516239022 },
body: { query: 'query Query() {\n }\n}\n' },
_body: true,
length: undefined,
[Symbol(kCapture)]: false,
[Symbol(RequestTimeout)]: undefined
}
after a little bit try and error, I can solve this issue by cast the context request to express-jwt.Request type as the code shown below,
const server = new ApolloServer({
schema,
context: (({ req }: { req: Request }) => {
console.log(req.auth);
return {};
}),
csrfPrevention: true,
cache: 'bounded',
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
],
});

How to enable graphql subscription in loopback 4 with openapi-to-graphql

as per the title, I am having problem trying to enable graphql subscription in my loopback 4 application.
Here is my code that I've done so far.
index.ts
export async function main(options: ApplicationConfig = {}) {
const app = new BackendLb4Application(options)
await app.boot()
await app.start()
const url = app.restServer.url;
const oas: Oas3 = <Oas3><unknown>await app.restServer.getApiSpec()
const {schema} = await createGraphQLSchema(oas, {
operationIdFieldNames: true,
baseUrl: url,
createSubscriptionsFromCallbacks: true,
})
const handler = graphqlHTTP( (request:any, response:any, graphQLParams: any) => ({
schema,
pretty: true,
graphiql: true
}))
app.mountExpressRouter(graphqlPath, handler);
const pubsub = new PubSub()
const ws = createServer(app);
ws.listen(PORT, () => {
new SubscriptionServer(
{
execute,
subscribe,
schema,
onConnect: (params: any, socket: any, ctx: any) => {
console.log(params, 'here on onconnect')
// Add pubsub to context to be used by GraphQL subscribe field
return { pubsub }
}
},
{
server: ws,
path: '/subscriptions'
}
)
})
return app
}
Here is my schema
type Subscription {
"""
Equivalent to PATCH onNotificationUpdate
"""
postRequestQueryCallbackUrlApiNotification(secondInputInput: SecondInputInput): String
"""
Equivalent to PATCH onNotificationUpdate
"""
postRequestQueryCallbackUrlOnNotificationUpdate(firstInputInput: FirstInputInput): String
}
Here is an example of my controller
#patch('/notification-update', {
operationId: 'notificationUpdate',
description: '**GraphQL notificationUpdate**',
callbacks:[ {
onNotificationUpdate: {
//'{$request.query.callbackUrl}/onNotificationUpdate': {
post: {
requestBody: {
operationId: 'notificationUpdateCallback',
description: 'rasjad',
content: {
'application/json': {
schema: {
title: "firstInput",
type: 'object',
properties: {
userData: {
type: "string"
}
}
}
}
}
},
responses: {
'200': {
description: 'response to subscription',
}
}
}
},
// }
}],
responses: {
'200': {
description: 'Notification PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Notification, {partial: true}),
},
},
})
notification: Notification,
#param.where(Notification) where?: Where<Notification>,
): Promise<Count> {
return this.notificationRepository.update(notification, where);
}
Ive defined the callbacks object in my controller which will then create a subscription in my schema. Tested it out on graphiql but did not work.
I am not sure where to go from here. Do I need a custom resolver or something? Not sure.
Appreciate it if anyone could help on this.
Just in case someone else is looking to do the same thing.
I switched out graphqlHTTP with Apollo Server to create my graphql server.
So my final index.ts looks like this.
export async function main(options: ApplicationConfig = {}) {
const lb4Application = new BackendLb4Application(options)
await lb4Application.boot()
await lb4Application.migrateSchema()
await lb4Application.start()
const url = lb4Application.restServer.url;
const graphqlPath = '/graphql'
// Get the OpenApiSpec
const oas: Oas3 = <Oas3><unknown>await lb4Application.restServer.getApiSpec()
// Create GraphQl Schema from OpenApiSpec
const {schema} = await createGraphQLSchema(oas, {
strict: false,
viewer: true,
baseUrl: url,
headers: {
'X-Origin': 'GraphQL'
},
createSubscriptionsFromCallbacks: true,
customResolvers: {
"lb4-title": {
"your-path":{
patch: (obj, args, context, info) => {
const num = Math.floor(Math.random() * 10);
pubsub.publish("something", { yourMethodName: {count: num} }).catch((err: any) => {
console.log(err)
})
return {count: 1}
}
}
}
},
customSubscriptionResolvers: {
"lb4-title" : {
"yourMethodName": {
post: {
subscribe: () => pubsub.asyncIterator("something"),
resolve: (obj: any, args: any, context, info) => {
console.log(obj, 'obj')
}
}
}
}
}
})
const app = express();
const server = new ApolloServer({
schema,
plugins: [{
async serverWillStart() {
return {
async drainServer() {
subscriptionServers.close();
}
};
}
}],
})
const subscriptionServers = SubscriptionServer.create(
{
// This is the `schema` we just created.
schema,
// These are imported from `graphql`.
execute,
subscribe,
},
{
server: lb4Application.restServer.httpServer?.server,
path: server.graphqlPath,
//path: server.graphqlPath,
}
);
await server.start();
server.applyMiddleware({ app, path: "/" });
lb4Application.mountExpressRouter('/graphql', app);
return lb4Application
}
Also you will need to define the callbacks object in your controller like so.
#patch('/something-update', {
operationId: 'somethingUpdate',
description: '**GraphQL somethingUpdate**',
callbacks:[
{
yourMethodName: {
post: {
responses: {
'200': {
description: 'response to subscription',
content: {'application/json': {schema: CountSchema}},
}
}
}
},
}
],
responses: {
'200': {
description: 'Something PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Something, {partial: true}),
},
},
})
something: Something,
#param.where(Something) where?: Where<Something>,
): Promise<Count> {
return this.somethingRepository.updateAll(something, where);
}
And that is it. You can test it out from the GraphQL Playground and play around with the subscriptions.
For the time being, I am fine with defining customResolvers and customSubscriptionResolvers but I'm pretty sure I can automate this two objects from the controllers.
Cheers!

throw a descriptive error with graphql and apollo

Consider the following class:
// entity/Account.ts
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'
import { Field, Int, ObjectType } from 'type-graphql'
#ObjectType()
#Entity()
export class Account extends BaseEntity {
#Field(() => Int)
#PrimaryGeneratedColumn()
id: number
#Field()
#Column({ length: 50, unique: true })
#Index({ unique: true })
accountIdentifier: string
#Field({ nullable: true })
#Column({ length: 100 })
name?: string
}
With it's corresponding resolver:
// AccountResolver.ts
#Resolver()
export class AccountResolver {
#Mutation(() => Account)
async addAccount(#Arg('options', () => AccountInput) options: AccountInput) {
try {
// if (!options.accountIdentifier) {
// throw new Error(`Failed adding account: the accountIdentifier is missing`)
// }
return await Account.create(options).save()
} catch (error) {
if (error.message.includes('Cannot insert duplicate key')) {
throw new Error(
`Failed adding account: the account already exists. ${error}`
)
} else {
throw new Error(`Failed adding account: ${error}`)
}
}
}
}
Jest test file
// AccountResolver.test.ts
describe('the addAccount Mutation', () => {
it('should throw an error when the accountIdentifier is missing', async () => {
await expect(
client.mutate({
mutation: gql`
mutation {
addAccount(
options: {
name: "James Bond"
userName: "James.Bond#contoso.com"
}
) {
accountIdentifier
}
}
`,
})
).rejects.toThrowError('the accountIdentifier is missing')
})
The field accountIdentifier is mandatory and should throw a descriptive error message when it's missing in the request. However, the error thrown is:
"Network error: Response not successful: Received status code 400"
What is the correct way to modify the error message? I looked at type-graphql with the class-validators and made sure that validate: true is set but it doesn't give a descriptive error.
EDIT
After checking the graphql playground, it does show the correct error message by default. The only question remaining is how write the jest test so it can read this message:
{
"error": {
"errors": [
{
"message": "Field AccountInput.accountIdentifier of required type String! was not provided.",
Thank you for any help you could give me.
The ApolloError returned by your client wraps both the errors returned in the response and any network errors encountered while executing the request. The former is accessible under the graphQLErrors property, the latter under the networkError property. Instea dof using toThrowError, you should use toMatchObject instead:
const expectedError = {
graphQLErrors: [{ message: 'the accountIdentifier is missing' }]
}
await expect(client.mutate(...)).rejects.toMatchObject(expectedError)
However, I would suggest avoiding using Apollo Client for testing. Instead, you can execute operations directly against your schema.
import { buildSchema } from 'type-graphql'
import { graphql } from 'graphql'
const schema = await buildSchema({
resolvers: [...],
})
const query = '{ someField }'
const context = {}
const variables = {}
const { data, errors } = await graphql(schema, query, {}, context, variables)

NestJS Context is undefined in graphql subscription

can someone help me, why is the CONTEXT undefined inside my subscription?
#Subscription(returns => CommentsDto, {
filter: (payload, variables, context) => {
console.log({ payload, variables, context }) // <------------ context context undefined
const isSameCode = variables.code === payload.newComment.code
const isAuthorized = context.req.headers.clientauthorization === payload.clientauthorization
return isSameCode && isAuthorized
},
})
newComment(
#Context() context,
#Args(({ name: 'code', type: () => String })) code: string,
) {
console.log(context) // <------------ undefined
return this.publisherService.asyncIterator('newComment')
}
It is working for Queries and Mutatinos...
Graphql definition is:
const GraphQLDefinition = GraphQLModule.forRoot({
context: ({ req, connection }) => {
// subscriptions
if (connection) {
return { req: connection.context }
}
// queries and mutations
return { req }
},
installSubscriptionHandlers: true,
path: '/graphql',
playground: true,
})
Thank you for any help
Because the Req and Res are undefined in the case of subscriptions so when you try to log the context it is undefined.
For context to be available you need to change the guards that you are using to return the context which can be found in the connection variable.
Basically to summarize:
=> req, res used in http/query & mutations
=> connection used in webSockets/subscriptions
Now to get the context correctly you will have to perform these steps exactly:
Modify App module file to use the GraphqlModuleImport
Modify Extract User Guard and Auth guard (or whatever guards you are using)
to return data for both query/mutation and subscription case.
Receive data using the context in the subscription.
Add jwtTokenPayload extractor function in the Auth service.
Opitonal: Helper Functions and DTOs for Typescript.
1-Detail:
GraphQLModule.forRootAsync({
//import AuthModule for JWT headers at graphql subscriptions
imports: [AuthModule],
//inject Auth Service
inject: [AuthService],
useFactory: async (authService: AuthService) => ({
debug: true,
playground: true,
installSubscriptionHandlers: true,
// pass the original req and res object into the graphql context,
// get context with decorator `#Context() { req, res, payload, connection }: GqlContext`
// req, res used in http/query&mutations, connection used in webSockets/subscriptions
context: ({ req, res, payload, connection }: GqlContext) => ({
req,
res,
payload,
connection,
}),
// subscriptions/webSockets authentication
typePaths: ["./**/*.graphql"],
resolvers: { ...resolvers },
subscriptions: {
// get headers
onConnect: (connectionParams: ConnectionParams) => {
// convert header keys to lowercase
const connectionParamsLowerKeys: Object = mapKeysToLowerCase(
connectionParams,
);
// get authToken from authorization header
let authToken: string | false = false;
const val = connectionParamsLowerKeys["authorization"];
if (val != null && typeof val === "string") {
authToken = val.split(" ")[1];
}
if (authToken) {
// verify authToken/getJwtPayLoad
const jwtPayload: JwtPayload = authService.getJwtPayLoad(
authToken,
);
// the user/jwtPayload object found will be available as context.currentUser/jwtPayload in your GraphQL resolvers
return {
currentUser: jwtPayload.username,
jwtPayload,
headers: connectionParamsLowerKeys,
};
}
throw new AuthenticationError("authToken must be provided");
},
},
definitions: {
path: join(process.cwd(), "src/graphql.classes.ts"),
outputAs: "class",
},
}),
}),
2-Detail:
My getRequest function example from the ExtractUserGuard class that extends the AuthGuard(jwt) class.
Change from:
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
return request;}
to this:
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
// req used in http queries and mutations, connection is used in websocket subscription connections, check AppModule
const { req, connection } = ctx.getContext();
// if subscriptions/webSockets, let it pass headers from connection.context to passport-jwt
const requestData =
connection && connection.context && connection.context.headers
? connection.context
: req;
return requestData;
}
3- Now you can get this data in your resolver.
#Subscription("testSubscription")
#UseGuards(ExtractUserGuard)
async testSubscription(
#Context("connection") connection: any,
): Promise<JSONObject> {
const subTopic = `${Subscriptions_Test_Event}.${connection.context.jwtPayload.email}`;
console.log("Listening to the event:", subTopic);
return this.pubSub.asyncIterator(subTopic);
}
4- For getting the jwtPayload using the token add the following function to your AuthService.
getJwtPayLoad(token: string): JwtPayload {
const jwtPayload = this.jwtService.decode(token);
return jwtPayload as JwtPayload;
}
5-Helper Functions and DTOs example (that I used in my project)
DTOs:
export interface JwtPayload {
username?: string;
expiration?: Date;
}
export interface GqlContext {
req: Request;
res: Response;
payload?: JwtPayload;
// required for subscription
connection: any;
}
export interface ConnectionParams {
authorization: string;
}
Helper Function:
export function mapKeysToLowerCase(
inputObject: Record<string, any>,
): Record<string, any> {
let key;
const keys = Object.keys(inputObject);
let n = keys.length;
const newobj: Record<string, any> = {};
while (n--) {
key = keys[n];
newobj[key.toLowerCase()] = inputObject[key];
}
return newobj;
}

Can't set context to resolvers in apollo server

Hello I'm new to GraphQl and to Apollo Server.
I would like to implement authentication on my project.
But
For some reason, I can't seem to set context on my resolvers in apollo server.
Here's my index
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const userId = jwtDecode(req.headers.authorization)
return userId.sub
}
})
And my query
Query: {
users: async (parent, args, context) => {
try {
console.log(context)
return await getUsers(context)
} catch (err) {
console.log(err)
throw new Error(err.message)
}
}
When I try to output the context the result is always like this...
{ injector:
Injector {
options:
{ name: 'index.ts_8346047369535445_SESSION',
injectorScope: 'SESSION',
hooks: [Array],
children: [] },
_classMap: Map {},
_factoryMap: Map {},
_applicationScopeInstanceMap:
Map {
Symbol(ModuleConfig.index.ts_8346047369535445) => undefined,
[Function] => undefined },
_sessionScopeInstanceMap: Map { [Function: ModuleSessionInfo] => [ModuleSessionInfo] },
_applicationScopeServiceIdentifiers:
[ Symbol(ModuleConfig.index.ts_8346047369535445), [Function] ],
_requestScopeServiceIdentifiers: [],
_sessionScopeServiceIdentifiers: [ [Function: ModuleSessionInfo] ],
_hookServiceIdentifiersMap: Map {},
_name: 'index.ts_8346047369535445_SESSION',
_injectorScope: 'SESSION',
_defaultProviderScope: 'SESSION',
........
What's returned inside the context function should always be an object. So you would do something like
context: ({ req }) => {
const { sub } = jwtDecode(req.headers.authorization)
return {
sub,
}
}
and then access the value inside the resolver by calling context.sub.
However, if you're using GraphQL Modules to create your schema, you should follow the library's documentation for configuring your context on a per-module basis.

Resources