A set of core GraphQL utilities that MakerX uses to build GraphQL APIs.
These utilities avoid dependencies on any particular GraphQL server or logging implementation, providing a standard set of behaviours to use across varying implementations.
Note: See explanation on *Express peer dependency below.
createContextFactory returns a function that creates your GraphQL context using a standard (extensible) representation, including:
logger: a logger instance to use downstream of resolvers, built by yourrequestLoggerfactory, which receives both the resolved request metadata and the resolveduserso you can enrich log output with user-derived fields (see Request logger)requestInfo: useful request info —source(httporsubscription),protocol(http/https/ws/wss),host,baseUrl,url, correlation/client headers, etc. Use it for per-request behaviour (multi-tenant apps), passing correlation headers downstream, etc. See Request infouser: an object representing the user or system identity (see User; defaults to aUserbuilt from JWT claims whencreateUseris omitted)- anything else you wish to add to the context via
augmentContext
context.ts
// define the extra stuff added to our app's context
type ExtraContext = {
services: Services
loaders: Loaders
}
// configure the createContext function
// TUser is inferred from `createUser`, TAugment is inferred from `augmentContext`'s return type
export const createContext = createContextFactory({
// keys of the user claims (JWT payload) to include in the request metadata passed to the requestLogger factory
claimsToLog: ['oid', 'aud', 'tid', 'azp', 'iss', 'scp', 'roles'],
// keys of the request info to include in the request metadata passed to the requestLogger factory
requestInfoToLog: ['origin', 'requestId', 'correlationId'],
// build the per-request logger; receives the request metadata and the resolved user
// e.g. enrich log output with user-derived fields like multi-tenant `instance`
requestLogger: (requestMetadata, user) => logger.child({ ...requestMetadata, instance: user?.instance }),
// resolve the user for each request — optional; omit to use the default User-from-JWT behaviour
// (required when you supply a narrower TUser generic)
createUser: async ({ claims }) => new AppUser(claims),
// build the rest of the app context — annotate the return type to lock in inference
augmentContext: (context): ExtraContext => {
const services = createServices(context)
const loaders = createLoaders(services)
return { services, loaders }
},
})
// derive the full context type from the factory's return type
export type GraphQLContext = Awaited<ReturnType<typeof createContext>>These examples show how you might map implementation-specific context functions to your implementation-agnostic context creation function (from step 1).
Note: examples assume that a JWT auth middleware has set req.user to the decoded token payload (claims). This is optional.
app.ts
// wire up the createContext function, providing `ContextInput` for apollo-server-express implementation)
const server = new ApolloServer({
...apolloServerConfig,
context: ({ req }) => createContext({ req, claims: req.user }),
})lambda.ts
// wire up the createContext function, providing `ContextInput` for apollo-server-lambda implementation
const server = new ApolloServer({
...apolloServerConfig,
context: ({ event, context, express: { req } }) =>
createContext({ req, claims: req.user, event: event as LambdaEvent, context: context as LambdaContext }),
})yoga.ts
// wire up the createContext function, providing `ContextInput` for graphql-yoga implementation
const graphqlServer = createServer({
...yogaServerConfig,
context: ({ req }) => createContext({ req, claims: req.user }),
})context.requestInfo is built for every request — both HTTP and websocket subscription connects — so downstream code can distinguish sources, rebuild URLs, pass through correlation headers, etc.
| Field | Description |
|---|---|
requestId |
x-request-id header if present, otherwise a freshly generated UUID. |
source |
'http' for regular requests, 'subscription' for websocket connects. |
protocol |
'http' / 'https' for HTTP, 'ws' / 'wss' for subscriptions (resolved via x-forwarded-proto or TLS socket encryption). |
host |
Hostname only (no port). Prefers x-forwarded-host, falls back to the Host header, then req.hostname (Express only). |
port |
Port parsed from x-forwarded-host / Host header when present; undefined otherwise. |
baseUrl |
Fully-qualified origin (scheme://host[:port]) with default ports stripped. For subscriptions the scheme is normalised to http(s) so the value composes with relative URLs. |
url |
req.originalUrl for HTTP, req.url for subscription connects. |
origin |
Origin header. |
referer |
Referer header. |
correlationId |
x-correlation-id header. |
arrLogId |
x-arr-log-id header (Azure Front Door / ARR). |
clientIp |
First value from x-forwarded-for, falling back to socket.remoteAddress. |
userAgent |
User-Agent header. |
You can add more via augmentRequestInfo(input). Lambda deployments also get functionName and awsRequestId when a LambdaContext is supplied.
Helpers are exported for custom wiring: buildBaseRequestInfo(req) (Express), buildConnectRequestInfo(req) (websocket IncomingMessage), and requestBaseUrl / connectRequestBaseUrl.
The requestLogger config accepts either a pre-built Logger or a factory (requestMetadata, user) => Logger.
The factory form runs per request and receives:
requestMetadata— an object containingrequest(the subset ofrequestInfoselected byrequestInfoToLog) anduser(the subset of claims selected byclaimsToLog)user— the resolveduservalue returned bycreateUser(typed as yourTUser)
This lets you enrich log output with fields derived from the resolved user, for example a multi-tenant instance id or an internal user id from your database, that aren't present on the raw JWT claims:
requestLogger: (requestMetadata, user) =>
logger.child({
...requestMetadata,
instance: user?.instance,
userId: user?.id,
}),By default, if claims (decoded token JwtPayload) are available, the GraphQLContext.user property will be set by constructing a User instance.
The User class adds some handy getters over raw claims (decodedJWT payload) and provides access to the JWT (access token) for on-behalf-of downstream authentication flows. Note this may represent a user or service principal (system) identity.
| Property | Description |
|---|---|
| claims | The decoded JWT payload, set via the RequestInput.user field. |
| token | The bearer token from the request authorization header. |
| The user's email via coalesced claim values: email, emails, preferred_username, unique_name, upn. | |
| name | The user's name (via the name or given_name and family_name claims). |
| id | The user's unique and immutable ID, useful for contextual differentiation e.g. session keys (the oid or sub claim). |
| scopes | The user's scopes, via the scp claim split into an array of scopes. |
| roles | The user's roles (via the roles claim). |
If you wish to customise your GraphQLContext.user object, provide a createUser function to override the default User creation.
const createUser: CreateUser = async ({ claims }) => {
const roles = await loadRoles(claims.email)
return { name: claims.name, email: claims.email, roles }
}
const graphqlServer = createServer({
...config,
context: ({ req }) => createContext({ req, claims: req.user, createUser }),
})Logs a GraphQL operation in a consistent format with the option of including any additional data. Top level and result level log data with null or undefined values will be omitted for berevity.
Refer to the GraphQLLogOperationInfo type for the definition of input.
This function can be used across implementations, e.g. in a GraphQL Envelop plugin or ApolloServer plugin.
This library includes a subscriptions module to provide simple setup using the GraphQL WS package.
-
Install subscriptions dependencies (optional peer dependencies of this package):
npm i graphql-ws ws -
Subscription context setup:
createSubscriptionContextFactoryreturns a function that creates an equivalent GraphQL context using input supplied to the graphql-ws Server context callback.Example showing both normal context + subscription context creation:
type ExtraContext = { services: Services; dataSource: DataSource; dataLoaders: DataLoaders } // the `context` arg is typed `GraphQLContext<Logger, RequestInfo, AppUser | undefined>` here — // TUser flows through from `createUser`, so just annotate the return type and let inference do the rest const augmentContext = (context: GraphQLContext<Logger, RequestInfo, AppUser | undefined>): ExtraContext => { const services = createServices(context) const dataLoaders = createDataLoaders() return { services, dataSource, dataLoaders } } // create a context using request based input — TUser / TAugment inferred from the config const createContext = createContextFactory({ claimsToLog, requestInfoToLog, requestLogger: (requestMetadata, user) => logger.child({ ...requestMetadata, instance: user?.instance }), createUser: ({ claims, req }) => findUpdateOrCreateUser(claims, req.headers.authorization?.substring(7)), augmentContext, }) // create a context using graphql-ws Server#context callback input const createSubscriptionContext = createSubscriptionContextFactory({ claimsToLog, requestInfoToLog, requestLogger: (requestMetadata, user) => logger.child({ ...requestMetadata, instance: user?.instance }), createUser: ({ claims, connectionParams }) => findUpdateOrCreateUser(claims, extractTokenFromConnectionParams(connectionParams)), augmentContext, }) // share one context type between query and subscription paths export type GraphQLContext = Awaited<ReturnType<typeof createContext>>
-
Create a subscriptions server, using the ws-server cleanup function in your server lifecycle.
The
useSubscriptionsServerfunction sets up:- Auth token validation as part of establishing (or rejecting) the connection (behaviour defined by
verifyTokenandrequireAuthargs) - GraphQL context creation
- Logging from the server
onConnect,onDisconnect,onOperation,onNextandonErrorcallbacks
Example for Apollo Server (
wsServerCleanupcalled in thedrainServerplugin callback):export const startApolloServer = async (app: Express, httpServer: http.Server) => { logger.info('Building schema') const schema = createSchema() logger.info('Initialising subscriptions websocket server') const wsServerCleanup = useSubscriptionsServer({ schema, httpServer, logger, createSubscriptionContext, jwtClaimsToLog: config.get('logging.userClaimsToLog'), requireAuth: true, verifyToken: (host, token) => verifyForHost(host, token, config.get('auth.bearer')), }) logger.info('Starting apollo server') const server = new ApolloServer<GraphQLContext>({ schema, plugins: plugins(httpServer, wsServerCleanup), introspection: true, csrfPrevention: true, }) await server.start()
- Auth token validation as part of establishing (or rejecting) the connection (behaviour defined by
-
For authorisation, clients can include a connection parameter named
authorizationorAuthorizationusing the HTTP header formatBearer <token>. Note: Apollo Sandbox will include anAuthorizationconnection parameter when you specify an HTTPAuthorizationheader via the UI.
isIntrospectionQuery: indicates whether the query is an introspection query, based on the operation name or query content.
ApolloServer v3 standardises on the Express request representation.
GraphQL Yoga uses the NodeJS http request representation, plus adds the Express version when using an Express server.
This library therefore takes a peer dependency on Express as the standard (common) request representation.
The ApolloServer v4 roadmap will standardise on the NodeJS http request representation.
This library may swap to the NodeJS http representation in a future version.