Log Query/Mutation actions to database for Auditing - graphql

My goal is to run some kind of webhook, cloud function or say I want to perform some kind of action after each query success or mutation success in graphql.
Means I want to log each and every action performed by users (kind of history of when what was created and updated).
How can this be implemented using some kind of middleware between graphql and DB (say mongo for now)?
Means that middleware should be responsible to run the logging action each time a query or mutation is called from front-end.
Tech stack being used is- Node, express, graphQl, Redis etc.
Any suggestions would really be appreciated.
Thanks
The solution I came up with was calling a function manually each time a query or mutate.

If you're using Apollo, you can utilize the formatResponse and formatError options for logging, as outlined in the docs.
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: error => {
console.log(error);
return error;
},
formatResponse: response => {
console.log(response);
return response;
},
});
Using an extension can allow you to hook into different phases of the GraphQL request and allow more granular logging. A simple example:
const _ = require('lodash')
const { GraphQLExtension } = require('graphql-extensions')
module.exports = class LoggingExtension extends GraphQLExtension {
requestDidStart(options) {
logger.info('Operation: ' + options.operationName)
}
willSendResponse(o) {
const errors = _.get(o, 'graphqlResponse.errors', [])
for (const error of errors) {
logger.error(error)
}
}
}
There's a more involved example here. You can then add your extension like this:
const server = new ApolloServer({
typeDefs,
resolvers,
extensions: [() => new YourExtension()]
});
If you're using express-graphql to serve your endpoint, your options are a bit more limited. There's still a formatError option, but no formatResponse. There is a way to pass in an extensions array as well, but the API is different from Apollo's. You can take a look at the repo for more info.

Related

Is it possible to query my running apollo graphqlserver locally, without using http?

I'm running a Graphql server from Apollo, and the objective is fetch some data. However, I need this data locally - on the same server. Is that possible, or is the only way to query the Apollo server using http?
I know that I could possible accomplish this without using GraphQl, and just access the data layer, but the thing is that I would like to benefit from:
Authorization
Dataloaders
Already built-in optimization in our Graphql Api
I already have a working solution where I just use node-fetch to query localhost, but it seems like quite a bit of overhead.
Yes it is possible!
Apollo makes the schema building and execution for you, but you can also do it yourself.
Here is a mini example based on the apollo-server-express package. I create the schema and then give it to the apollo-server. Look below the server startup, I also create a query-string, then parse it and execute it without apollo and without an http request.
const express = require('express');
const { ApolloServer, gql, makeExecutableSchema } = require('apollo-server-express');
const { parse } = require('graphql/language')
const { execute } = require('graphql')
// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}
`;
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
async function startApolloServer() {
const server = new ApolloServer({ schema });
await server.start();
const app = express();
server.applyMiddleware({ app });
await new Promise(resolve => app.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
return { server, app };
}
startApolloServer()
const query = `
query {
hello
}
`
const document = parse(query)
const res = execute({
schema,
document,
})
console.log('res no request:', res)
if you run it, install apollo-server-express and graphql with npm and you are good to go
To execute you can pass all your request logic as well:
execute({
schema,
document,
rootValue: {},
contextValue: {
userInfo,
dbClient,
},
variableValues: body.variables,
}),
It is highly useful also if you want to test you server. If you need to do subscriptions you can use the subscribe method imported from graphql as well.

Use Apollo server as a pass through of a query from client

I have a use case where I have apollo-server-express running with a React based apollo-client. I have an external graphql-datasource for some queries. Currently, I've configured apollo-datasource-graphql to be used as a data source for my apollo-server-express. However, this requires duplication of work on the resolver in Apollo as well as the resolver on my external graphql system.
Is there a way for me to pass queries made in the client through the Apollo Server and to the external graphql data source?
Maybe you could access the GraphQL AST from the fourth resolver argument (resolveInfo) and pass it into a GraphQL client?
Here is some prototype code:
import { print } from 'graphql/language/printer';
function forwardOperationResolver(root, args, context, resolveInfo) {
return fetch('https://remote.host/graphql', {
method: 'POST',
body: JSON.stringify({
query: print(resolveInfo.operation),
variables: resolverInfo.variableValues,
}),
})
.then(response => response.json())
.then(response => {
if (response.errors) {
// Handle errors
}
return response.data;
});
}
Downside: This breaks a few things that usually work in GraphQL like partial results and error locations...

GraphQL subscription using server-sent events & EventSource

I'm looking into implementing a "subscription" type using server-sent events as the backing api.
What I'm struggling with is the interface, to be more precise, the http layer of such operation.
The problem:
Using the native EventSource does not support:
Specifying an HTTP method, "GET" is used by default.
Including a payload (The GraphQL query)
While #1 is irrefutable, #2 can be circumvented using query parameters.
Query parameters have a limit of ~2000 chars (can be debated)
which makes relying solely on them feels too fragile.
The solution I'm thinking of is to create a dedicated end-point for each possible event.
For example: A URI for an event representing a completed transaction between parties:
/graphql/transaction-status/$ID
Will translate to this query in the server:
subscription TransactionStatusSubscription {
status(id: $ID) {
ready
}
}
The issues with this approach is:
Creating a handler for each URI-to-GraphQL translation is to be added.
Deploy a new version of the server
Loss of the flexibility offered by GraphQL -> The client should control the query
Keep track of all the end-points in the code base (back-end, front-end, mobile)
There are probably more issues I'm missing.
Is there perhaps a better approach that you can think of?
One the would allow a better approach at providing the request payload using EventSource?
Subscriptions in GraphQL are normally implemented using WebSockets, not SSE. Both Apollo and Relay support using subscriptions-transport-ws client-side to listen for events. Apollo Server includes built-in support for subscriptions using WebSockets. If you're just trying to implement subscriptions, it would be better to utilize one of these existing solutions.
That said, there's a library for utilizing SSE for subscriptions here. It doesn't look like it's maintained anymore, but you can poke around the source code to get some ideas if you're bent on trying to get SSE to work. Looking at the source, it looks like the author got around the limitations you mention above by initializing each subscription with a POST request that returns a subscription id.
As of now you have multiple Packages for GraphQL subscription over SSE.
graphql-sse
Provides both client and server for using GraphQL subscription over SSE. This package has a dedicated handler for subscription.
Here is an example usage with express.
import express from 'express'; // yarn add express
import { createHandler } from 'graphql-sse';
// Create the GraphQL over SSE handler
const handler = createHandler({ schema });
// Create an express app serving all methods on `/graphql/stream`
const app = express();
app.use('/graphql/stream', handler);
app.listen(4000);
console.log('Listening to port 4000');
#graphql-sse/server
Provides a server handler for GraphQL subscription. However, the HTTP handling is up to u depending of the framework you use.
Disclaimer: I am the author of the #graphql-sse packages
Here is an example with express.
import express, { RequestHandler } from "express";
import {
getGraphQLParameters,
processSubscription,
} from "#graphql-sse/server";
import { schema } from "./schema";
const app = express();
app.use(express.json());
app.post(path, async (req, res, next) => {
const request = {
body: req.body,
headers: req.headers,
method: req.method,
query: req.query,
};
const { operationName, query, variables } = getGraphQLParameters(request);
if (!query) {
return next();
}
const result = await processSubscription({
operationName,
query,
variables,
request: req,
schema,
});
if (result.type === RESULT_TYPE.NOT_SUBSCRIPTION) {
return next();
} else if (result.type === RESULT_TYPE.ERROR) {
result.headers.forEach(({ name, value }) => res.setHeader(name, value));
res.status(result.status);
res.json(result.payload);
} else if (result.type === RESULT_TYPE.EVENT_STREAM) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
});
result.subscribe((data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
req.on('close', () => {
result.unsubscribe();
});
}
});
Clients
The two packages mentioned above have companion clients. Because of the limitation of the EventSource API, both packages implement a custom client that provides options for sending HTTP Headers, payload with post, what the EvenSource API does not support. The graphql-sse comes together with it client while the #graphql-sse/server has companion clients in a separate packages.
graphql-sse client example
import { createClient } from 'graphql-sse';
const client = createClient({
// singleConnection: true, use "single connection mode" instead of the default "distinct connection mode"
url: 'http://localhost:4000/graphql/stream',
});
// query
const result = await new Promise((resolve, reject) => {
let result;
client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
},
);
});
// subscription
const onNext = () => {
/* handle incoming values */
};
let unsubscribe = () => {
/* complete the subscription */
};
await new Promise((resolve, reject) => {
unsubscribe = client.subscribe(
{
query: 'subscription { greetings }',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
;
#graphql-sse/client
A companion of the #graphql-sse/server.
Example
import {
SubscriptionClient,
SubscriptionClientOptions,
} from '#graphql-sse/client';
const subscriptionClient = SubscriptionClient.create({
graphQlSubscriptionUrl: 'http://some.host/graphl/subscriptions'
});
const subscription = subscriptionClient.subscribe(
{
query: 'subscription { greetings }',
}
)
const onNext = () => {
/* handle incoming values */
};
const onError = () => {
/* handle incoming errors */
};
subscription.susbscribe(onNext, onError)
#gaphql-sse/apollo-client
A companion package of the #graph-sse/server package for Apollo Client.
import { split, HttpLink, ApolloClient, InMemoryCache } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { ServerSentEventsLink } from '#graphql-sse/apollo-client';
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
const sseLink = new ServerSentEventsLink({
graphQlSubscriptionUrl: 'http://localhost:4000/graphql',
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
sseLink,
httpLink
);
export const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
If you're using Apollo, they support automatic persisted queries (abbreviated APQ in the docs). If you're not using Apollo, the implementation shouldn't be too bad in any language. I'd recommend following their conventions just so your clients can use Apollo if they want.
The first time any client makes an EventSource request with a hash of the query, it'll fail, then retry the request with the full payload to a regular GraphQL endpoint. If APQ is enabled on the server, subsequent GET requests from all clients with query parameters will execute as planned.
Once you've solved that problem, you just have to make a server-sent events transport for GraphQL (should be easy considering the subscribe function just returns an AsyncIterator)
I'm looking into doing this at my company because some frontend developers like how easy EventSource is to deal with.
There are two things at play here: the SSE connection and the GraphQL endpoint. The endpoint has a spec to follow, so just returning SSE from a subscription request is not done and needs a GET request anyway. So the two have to be separate.
How about letting the client open an SSE channel via /graphql-sse, which creates a channel token. Using this token the client can then request subscriptions and the events will arrive via the chosen channel.
The token could be sent as the first event on the SSE channel, and to pass the token to the query, it can be provided by the client in a cookie, a request header or even an unused query variable.
Alternatively, the server can store the last opened channel in session storage (limiting the client to a single channel).
If no channel is found, the query fails. If the channel closes, the client can open it again, and either pass the token in the query string/cookie/header or let the session storage handle it.

Log apollo-server GraphQL query and variables per request

When using apollo-server 2.2.1 or later, how can one log, for each request, the query and the variables?
This seems like a simple requirement and common use case, but the documentation is very vague, and the query object passed to formatResponse no longer has the queryString and variables properties.
Amit's answer works (today), but IMHO it is a bit hacky and it may not work as expected in the future, or it may not work correctly in some scenarios.
For instance, the first thing that I thought when I saw it was: "that may not work if the query is invalid", it turns out that today it does work when the query is invalid. Because with the current implementation the context is evaluated before the the query is validated. However, that's an implementation detail that can change in the future. For instance, what if one day the apollo team decides that it would be a performance win to evaluate the context only after the query has been parsed and validated? That's actually what I was expecting :-)
What I'm trying to say is that if you just want to log something quick in order to debug something in your dev environment, then Amit's solution is definitely the way to go.
However, if what you want is to register logs for a production environment, then using the context function is probably not the best idea. In that case, I would install the graphql-extensions and I would use them for logging, something like:
const { print } = require('graphql');
class BasicLogging {
requestDidStart({queryString, parsedQuery, variables}) {
const query = queryString || print(parsedQuery);
console.log(query);
console.log(variables);
}
willSendResponse({graphqlResponse}) {
console.log(JSON.stringify(graphqlResponse, null, 2));
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
extensions: [() => new BasicLogging()]
});
Edit:
As Dan pointed out, there is no need to install the graphql-extensions package because it has been integrated inside the apollo-server-core package.
With the new plugins API, you can use a very similar approach to Josep's answer, except that you structure the code a bit differently.
const BASIC_LOGGING = {
requestDidStart(requestContext) {
console.log("request started");
console.log(requestContext.request.query);
console.log(requestContext.request.variables);
return {
didEncounterErrors(requestContext) {
console.log("an error happened in response to query " + requestContext.request.query);
console.log(requestContext.errors);
}
};
},
willSendResponse(requestContext) {
console.log("response sent", requestContext.response);
}
};
const server = new ApolloServer(
{
schema,
plugins: [BASIC_LOGGING]
}
)
server.listen(3003, '0.0.0.0').then(({ url }) => {
console.log(`GraphQL API ready at ${url}`);
});
If I had to log the query and variables, I would probably use apollo-server-express, instead of apollo-server, so that I could add a separate express middleware before the graphql one that logged that for me:
const express = require('express')
const { ApolloServer } = require('apollo-server-express')
const { typeDefs, resolvers } = require('./graphql')
const server = new ApolloServer({ typeDefs, resolvers })
const app = express()
app.use(bodyParser.json())
app.use('/graphql', (req, res, next) => {
console.log(req.body.query)
console.log(req.body.variables)
return next()
})
server.applyMiddleware({ app })
app.listen({ port: 4000}, () => {
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
})
Dan's solution mostly resolves the problem but if you want to log it without using express,
you can capture it in context shown in below sample.
const server = new ApolloServer({
schema,
context: params => () => {
console.log(params.req.body.query);
console.log(params.req.body.variables);
}
});
I found myself needing something like this but in a more compact form - just the query or mutation name and the ID of the user making the request. This is for logging queries in production to trace what the user was doing.
I call logGraphQlQueries(req) at the end of my context.js code:
export const logGraphQlQueries = ( req ) => {
// the operation name is the first token in the first line
const operationName = req.body.query.split(' ')[0];
// the query name is first token in the 2nd line
const queryName = req.body.query
.split('\n')[1]
.trim()
.split(' ')[0]
.split('(')[0];
// in my case the user object is attached to the request (after decoding the jwt)
const userString = req.user?.id
? `for user ${req.user.id}`
: '(unauthenticated)';
console.log(`${operationName} ${queryName} ${userString}`);
};
This outputs lines such as:
query foo for user e0ab63d9-2513-4140-aad9-d9f2f43f7744
Apollo Server exposes a request lifecycle event called didResolveOperation at which point the requestContext has populated properties called operation and operationName
plugins: [
{
requestDidStart(requestContext) {
return {
didResolveOperation({ operation, operationName }) {
const operationType = operation.operation;
console.log(`${operationType} recieved: ${operationName}`)
}
};
}
}
]
// query recieved: ExampleQuery
// mutation recieved: ExampleMutation

Apollo client: Can #defer be used with client side resolvers?

For some reason, I had to build a client-side only GraphQL server, my schema is built as follow:
private buildSchema(): GraphQLSchema {
const allTypes: string = ...// my types
const allResolvers: IResolvers[] = ...// my resolvers
return makeExecutableSchema({
typeDefs: allTypes,
resolvers: allResolvers
});
}
The client is as follow:
this.client = new ApolloClient({
link: new SchemaLink({schema: this.buildSchema()}),
cache: new InMemoryCache({
addTypename: false
})
});
And everything works fine except that my queries are not defered. For instance if I run:
const gqlQuery: string = `
{
user {
name
slowResolver #defer {
text
}
}
}
`
const $result = this.apollo.getClient().watchQuery({
query: gql(gqlQuery)
});
The $result will be emited only when the whole query will be resolved (instead of user and then slowResolver as expected).
Any idea of what I missed in the workflow?
The #defer directive was actually removed from Apollo, although there's been some work done to reimplement it. Even if it's implemented, though, deferred queries would have to be handled outside of the execution context. In other words, executing the schema can return a deferred execution result, but something else (like Apollo server itself) has to handle how that response (both the initial payload, and the subsequent patches) are actually sent to the server over whatever transport.
If you're defining a schema client-side, unfortunately, it's not going to be possible to use the #defer directive.

Resources