Request logging in Apollo GraphQL - node.js

Environment: Node app based on Apollo GraphQL server (direct Apollo server, no express middleware)
I need to intercept requests and log log them at certain points of the processing pipelines. Here is what I have so far:
const server = new ApolloServer({
// code removed for clarity
context: async ({ req }) => {
// here is the first request log, preparing the context for the upcoming calls (traceability)
},
formatError: async (err: any) => {
// Here I would like to finish logging, but no context is available
},
Problems are traceability of different logs from the same end user request and logging of the successful requests.
1- How can I relate the request context within the formatError method?
2- Where should I implement the logging of the successfully executed requests?

Aleks,
You can create custom error classes which can be extended from ApolloError or node Error classes. By assigning context to the error object from the resolver function, you can use it from formatError method.
formatResponse method can be used here which provides requestContext as as argument.
formatError and formatResponse methods usage:
const { ApolloServer, gql, makeExecutableSchema } = require("apollo-server");
const typeDefs = gql`
type User {
name: String
username: String
statusCode: Int
}
type Query {
user: User
}
`;
const dummyUser = { username: "dummyUser", name: "Dummy user" };
const user = () => {
// throw new Error('Test error');
return dummyUser;
};
const resolvers = {
Query: { user }
};
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
const server = new ApolloServer({
context: ({ req }) => ({ headers: req.headers }),
schema,
introspection: true,
playground: true,
formatResponse: (response) => {
response.data.user.statusCode = 200;
return response;
},
formatError: (error) => {
error.message = "Error name";
return error;
}
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Trying out in middleware might be an alternate approach for both(not sure about this).

Related

Steam authentication with API Gateway and lambda

I'm trying to create the authentication of my website using
https://github.com/LeeviHalme/node-steam-openid.
Steam OpenID: https://partner.steamgames.com/doc/features/auth
I have an API Gateway with these two endpoints:
/login
// the steamAuth file is the same module as node-steam-openid but for ts
import { SteamAuth } from "../utils/steamAuth";
export const login = async () => {
const client = new SteamAuth(
'http://localhost:3000',
`${process.env.API_URL}/consume`,
process.env.STEAM_API_KEY,
);
try {
const redirectUrl = await client.getRedirectUrl();
return {
statusCode: 302,
headers: { Location: redirectUrl }
};
} catch (e) {
console.log(e);
return {
statusCode: 500,
message: 'Internal server error'
};
}
}
/consume
import { APIGatewayEvent } from 'aws-lambda';
import { SteamAuth } from "../utils/steamAuth";
export const consume = async (event: APIGatewayEvent) => {
const client = new SteamAuth(
'http://localhost:3000',
`${process.env.API_URL}/consume`,
process.env.STEAM_API_KEY,
);
console.log(event);
try {
const user = await client.authenticate(event);
console.log('success', user);
} catch (e) {
console.log('error', e);
}
return {
statusCode: 302,
headers: { Location: 'http://localhost:3000/' },
};
}
The thing is I get this error in /consume endpoint
error TypeError: Cannot read property 'toUpperCase' of undefined
at Object.openid.verifyAssertion (/var/task/node_modules/openid/openid.js:905:28)
at openid.RelyingParty.verifyAssertion (/var/task/node_modules/openid/openid.js:68:10)
at /var/task/src/utils/steamAuth.js:60:31
at new Promise (<anonymous>)
at SteamAuth.authenticate (/var/task/src/utils/steamAuth.js:59:16)
at Runtime.consume [as handler] (/var/task/src/lambda/consume.js:9:35)
at Runtime.handleOnceNonStreaming (/var/runtime/Runtime.js:73:25)
I believe this error occurs because the verifyAssertion is waiting for an express request while it is provided an API Gateway one.
Link to the code with the mentioned function is here
Should I use another module to do the authentication as I don't really want to modify the source code of the module? I didn't find anything at the moment
Thanks!
I found a workaround using express in lambda. As expected, the openid module used by node-steam-openid is expecting an express request and not a lambda event.
import { SteamAuth } from "../utils/steamAuth";
const express = require('express');
const serverless = require('serverless-http');
const app = express();
app.get('/verify', async (req: any, res: any) => {
const client = new SteamAuth(
process.env.HOSTNAME,
`${process.env.API_URL}/verify`,
process.env.STEAM_API_KEY,
);
try {
const user: any = await client.authenticate(req);
} catch (e) {
throw new Error(e.message);
}
});
module.exports.verify = serverless(app);

Learn about apollo-graphql Subscription

I'm presently studying Graphql(apollo) and utilising Node.js for the backend.
I've made some progress with queries and mutations, but I'm stuck at subscription. I've seen several videos and read some blogs, but I'm having trouble grasping it since they're utilising front-end frameworks, such as react, and I'm not familiar with react or any other front-end javascript frameworks.
I'm solely interested in learning it for the backend.
Is there anyone who can assist me with this?
I've got three separate queries (or whatever they're called) that I'm working on.
User,
Post and
Comment
Now I want to generate a subscription whenever someone adds a new comment or creates a new post.
For users, comments, and posts, I already have a mutation for add, update and remove.
There is currently no authentication or authorization in place.
Your question is more to help me implement this rather than actually asking an issue or problem you faced after trying out some steps in series of order.
This might help you when learning and implementing GraphQL subscription;
https://www.apollographql.com/docs/apollo-server/data/subscriptions/
https://www.youtube.com/watch?v=R2IrBI4-7Kg&t=698s
If you have tried something and faced a wall, please add your code and what you have done, so I could help you on where you are wrong or what you could do hopefully. Cheers.
The solutions I'm providing are crafted on #apollo/server v.4, with expressMiddleware and mongodb/mongoose on the backend and subscribeToMore with updateQuery on the client-side instead of the useSubscription hook.
The graphql-transport-ws transport library is no longer supported; instead, use graphql-ws.
The implementation differentiates three main collections: User, Post, and Comment, as well as subscription for post creation, post modification, and user authentication.
Likewise, as of 12.2022, the following setup and configuration apply.
Subscription on the backend:
Install the following dependencies:
$ npm i #apollo/server #graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express
I'm assuming you've already configured your MongoDB models; if not, you might want to look at this repo for a basic setup.
Set up schema types and resolvers, such as this one.
// typeDefs.js
const typeDefs = `#graphql
type User {
id: ID!
email: String!
posts: [Post]!
commentsMade: [Comment]!
}
type Token {
value: String!
}
input UpdatePostInput {
title: String!
}
type Post {
id: ID!
title: String!
postedBy: User
comments: [Comment]!
}
input CommentInput {
text: String!
}
type Comment {
id: ID!
text: String!
commenter: User!
commentFor: Post!
}
type Query {
users: [User]!
user(id: ID!): User!
posts: [Post!]!
post(id: ID!): Post!
comment(id: ID!): Comment!
comments: [Comment!]!
}
type Mutation {
signup(email: String!, password: String!): User
signin(email: String!, password: String!): Token
createPost(title: String): Post
createComment(postId: String!, commentInput: CommentInput!): Comment
updatePost(postId: ID!, updatePostInput: UpdatePostInput!): Post
}
type Subscription {
postAdded: Post
commentAdded: Comment
postUpdated: Post
}
`
export default typeDefs
// resolvers.js
import dotenv from 'dotenv'
import { PubSub } from 'graphql-subscriptions'
import mongoose from 'mongoose'
import { GraphQLError } from 'graphql'
import bcrypt from 'bcrypt'
import UserModel from '../models/User.js'
import PostModel from '../models/Post.js'
import CommentModel from '../models/Comment.js'
dotenv.config()
...
const pubsub = new PubSub()
const User = UserModel
const Post = PostModel
const Comment = CommentModel
const secret = process.env.TOKEN_SECRET
const resolvers = {
Query: {...},
Mutation: {
...
createPost: async (_, args, contextValue) => {
const authUser = contextValue.authUser
if (!authUser) {
throw new GraphQLError('User is not authenticated', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
})
}
const post = new Post({
...args,
postedBy: mongoose.Types.ObjectId(authUser.id),
})
try {
const savedPost = await post.save()
authUser.posts = authUser.posts.concat(post._id)
await authUser.save()
const addedPost = {
id: savedPost.id,
title: savedPost.title,
postedBy: savedPost.postedBy,
comments: savedPost.comments,
}
// subscription postAdded with object iterator POST_ADDED
pubsub.publish('POST_ADDED', { postAdded: addedPost })
return post
} catch (error) {
throw new GraphQLError(`Error: ${error.message}`, {
extensions: {
code: 'BAD_USER_INPUT',
http: { status: 400 },
argumentName: args,
},
})
}
},
updatePost: async (_, args, contextValue) => {
const authUser = contextValue.authUser
if (!authUser) {
throw new GraphQLError('User is not authenticated', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
})
}
try {
const post = await Post.findByIdAndUpdate(
args.postId,
args.updatePostInput,
{ new: true }
)
.populate('comments')
.populate('postedBy')
const updatedPost = {
id: post.id,
title: post.title,
postedBy: post.postedBy,
comments: post.comments,
}
// subscription postUpdated with object iterator POST_UPDATED
pubsub.publish('POST_UPDATED', { postUpdated: updatedPost })
return post
} catch (error) {
throw new GraphQLError(`Error: ${error.message}`, {
extensions: {
code: 'BAD_REQUEST',
http: { status: 400 },
argumentName: args,
},
})
}
},
},
// resolvers for post addition and post modification using subscribe function
Subscription: {
postAdded: {
subscribe: () => pubsub.asyncIterator('POST_ADDED'),
},
commentAdded: {
subscribe: () => pubsub.asyncIterator('COMMENT_ADDED'),
},
postUpdated: {
subscribe: () => pubsub.asyncIterator('POST_UPDATED'),
},
},
//Hard-coding the default resolvers is appropriate in some cases,
// but I think it is required in fields with references to other
//database models to avoid returning null field values.
Post: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
title: async (parent) => {
return parent.title
},
postedBy: async (parent) => {
const user = await User.findById(parent.postedBy)
.populate('posts', { id: 1, title: 1, comments: 1, postedBy: 1 })
.populate('commentsMade')
//console.log('id', user.id)
//console.log('email', user.email)
return user
},
comments: async (parent) => {
return parent.comments
},
},
Comment: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
text: async (parent, args, contextValue, info) => {
return parent.text
},
commenter: async (parent, args, contextValue, info) => {
const user = await User.findById(parent.commenter)
.populate('posts')
.populate('commentsMade')
return user
},
commentFor: async (parent, args, contextValue, info) => {
const post = await Post.findById(parent.commentFor)
.populate('comments')
.populate('postedBy')
return post
},
},
...
}
export default resolvers
The code in the main entry server file (e.g. index.js) may look like this, e.g.
import dotenv from 'dotenv'
import { ApolloServer } from '#apollo/server'
import { expressMiddleware } from '#apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '#apollo/server/plugin/drainHttpServer'
import { makeExecutableSchema } from '#graphql-tools/schema'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import express from 'express'
import http from 'http'
import cors from 'cors'
import bodyParser from 'body-parser'
import jwt from 'jsonwebtoken'
import UserModel from './models/User.js'
import typeDefs from './tpeDefs.js'
import resolvers from './resolvers.js'
import mongoose from 'mongoose'
dotenv.config()
mongoose.set('strictQuery', false)
let db_uri
if (process.env.NODE_ENV === 'development') {
db_uri = process.env.MONGO_DEV
}
mongoose.connect(db_uri).then(
() => {
console.log('Database connected')
},
(err) => {
console.log(err)
}
)
const startGraphQLServer = async () => {
const app = express()
const httpServer = http.createServer(app)
const schema = makeExecutableSchema({ typeDefs, resolvers })
const wsServer = new WebSocketServer({
server: httpServer,
path: '/',
})
const serverCleanup = useServer({ schema }, wsServer)
const server = new ApolloServer({
schema,
context: async ({ req }) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token) {
const decoded = jwt.verify(token, process.env.TOKEN_SECRET)
const authUser = await UserModel.findById(decoded.id)
.populate('commentsMade')
.populate('posts')
return { authUser }
}
},
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose()
},
}
},
},
],
})
await server.start()
app.use(
'/',
cors(),
bodyParser.json(),
expressMiddleware(server, {
context: async ({ req }) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token) {
const decoded = jwt.verify(token, process.env.TOKEN_SECRET)
const authUser = await UserModel.findById(decoded.id)
.populate('commentsMade')
.populate('posts')
return { authUser }
}
},
})
)
const PORT = 4000
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}`)
)
}
startGraphQLServer()
END. It's time to run some tests and checks in the Apollo Explorer sandbox. Be conscientious about defining the required default resolvers to avert the Apollo server from sending null values on your behalf.
To view the code and implementation, go to this repository.
Happy coding!

How to use Redux to dispatch data to the backend (and consequently mongoDB)?

I recently created a simple MERN application that is supposed to use a form to send data to the backend using Redux to maintain state management. I'm new to Redux (as you will see in my code) and I believe I must have messed up the dispatching.
Below are the functions in my Form component:
const [landlordData, setLandlordData] = useState({name: '', type: '', rating: '', details: ''});
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
console.log(landlordData);
dispatch(createLandlord(landlordData));
}
Which console log the data from the form normally. When I submit though the new entry in the MongoDB only includes the time created and the UUID of the entry due to the Schema of the database:
import mongoose from 'mongoose';
const landlordSchema = mongoose.Schema({
name: String,
type: String,
rating: Number,
details: String,
createdAt: {
type: Date,
default: new Date()
}
});
var landlordDetails = mongoose.model('Landlords', landlordSchema);
export default landlordDetails;
To provide more context on the backend operations here is the controller script I made:
import landlordDetails from '../models/landlords.js';
export const getLandlords = async (req, res) => {
try {
const getDetails = await landlordDetails.find();
console.log(getDetails);
res.status(200).json(getDetails);
} catch (error) {
res.status(404).json({ message: error.message });
}
}
export const createLandlords = async (req, res) => {
const details = req.body;
const newLandlord = new landlordDetails(details);
try {
await newLandlord.save();
res.status(201).json(newLandlord);
console.log("New landlord added!");
} catch (error) {
res.status(409).json({ message: error.message })
}
}
Please let me know if any more information is needed or if I am completely oblivious to something obvious. Thank you.
EDIT: To provide more context, here are my api calls and my action script:
API:
import axios from 'axios';
const url = 'http://localhost:5000/landlords';
export const fetchLandlords = () => axios.get(url);
export const createLandlord = (landlordData) => axios.post(url, landlordData);
Actions JS file:
import * as api from '../api/index.js';
//Action creators
export const getLandlords = () => async (dispatch) => {
try {
const { data } = await api.fetchLandlords();
dispatch({ type: 'FETCH_ALL', payload: data });
} catch (error) {
console.log(error.message);
}
};
export const createLandlord = (landlord) => async (dispatch) => {
try {
const { data } = await api.createLandlord(landlord);
dispatch({ type: 'CREATE', payload: data });
} catch (error){
console.log(error);
}
};
When I click the submit button, a new database entry is made with the createdAt field but nothing else.

Error: Apollo Server requires either an existing schema, modules or typeDefs

I have this function that I want to test, ina nodeJS project that uses Apollo Server's federated gateway implementation.
#Service()
export class Server {
constructor();
}
async startAsync(): Promise<void> {
await this.createApolloGateway();
}
private async createApolloGateway(): Promise<void> {
const gateway = new ApolloGateway({
serviceList: [{ name: 'products', url: 'https://products-service.dev/graphql' },
{ name: 'reviews', url: 'https://reviews-service.dev/graphql' }]});
const {schema, executor} = await gateway.load();
const server = new ApolloServer({schema, executor});
return new Promise((resolve, _) => {
server.listen(8080).then(({url}) => {
resolve();
});
});
}
}
but when I test this function I have this error:
Error: Apollo Server requires either an existing schema, modules or typeDefs
I have tride to mock the schema doing this in jest framework
apolloGateway = createMockInstance(ApolloGateway);
it('should start an http server', async () => {
// Arrange
const server = new Server(configurationService, loggerService);
const schema = `
type User {
id: ID!
name: String
lists: [List]
}
type RootQuery {
user(id: ID): User
}
schema {
query: RootQuery
}
`;
apolloGateway.load = jest.fn().mockReturnValue(schema);
// Act
await server.startAsync();
// Assert
await expect(server).not.toBeNull;
}, 30000);
but I have the same error
Update the apollo-server to least version and pass only gateway when create apollo server.
const server = new ApolloServer({
gateway
});

Test a secure graphql subscription

I am trying to test a configuration for securing graphql subscriptions in my application.
This is my config in the ApolloServer constructor:
const app = express();
const jwt_authentication = jwt({
secret: JWT_SECRET,
credentialsRequired: false
})
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
formatError: error => {
console.log(error);
},
context: async ({req, connection }) => {
if (connection) {
return connection.context;
} else {
return some_method_to_return_user_info;
}
},
subscriptions: {
onConnect: async (connectionParams, webSocket, context) => {
const user = await jsonwebtoken.verify(connectionParams.jwt, JWT_SECRET);
const userInfo= some_method_to_return_user_info;
if (userInfo) {
return { user: userInfo };
}
throw new Error("Unauthorized subscription");
}
}
});
app.use(GRAPHQL_PATH, jwt_authentication);
//...
When I run a subscription in GraphQL Playground I get the error:
jwt must be provided
I tested with the header "Authorization": "Bearer MY_TOKEN" and then with "jwt": "MY_TOKEN", but I believe that it's not as straightforward as that.
Is there any possibility to test my subscriptions without implementing a client code?
I got it working in GraphQL Playground by adding the HTTP Header that way:
{
"jwt": "MY_TOKEN"
}

Resources