Mercurius

repository·master·Indexed 25 days ago

https://github.com/mercurius-js/mercurius

A high-performance GraphQL adapter for the Fastify web framework. Mercurius provides features including query caching, automatic loader integration to prevent N+1 problems, subscriptions, batched queries, and support for Federation. It integrates with tools like Prisma, NestJS, and OpenTelemetry, and offers a configurable GraphiQL IDE with support for custom UMD plugins.

Tokens
31.1K
Snippets
70
Records
111
Agent score
79%

What's inside mercurius

  1. Available Mercurius Integrations

    master

    Mercurius supports several integrations to streamline your GraphQL development workflow, including schema construction tools, ORMs, testing utilities, and observability frameworks.

    Supported integrations include:

    • Schema Construction: nexus, TypeGraphQL, and GQLoom for code-first, strongly typed GraphQL schemas.
    • Data Access: Prisma ORM.
    • Testing: mercurius-integration-testing for writing integration tests.
    • Observability: @opentelemetry for traces and metrics.
    • Frameworks: NestJS for modularized TypeScript applications.
  2. List of related Mercurius plugins

    master

    The following plugins are available to extend Mercurius functionality:

    • mercurius-auth: Adds configurable Authentication and Authorization support.
    • mercurius-cache: Caches the results of GraphQL resolvers.
    • mercurius-validation: Adds configurable validation support.
    • mercurius-upload: Provides File upload support via graphql-upload implementation.
    • altair-fastify-plugin: A GraphQL Client IDE (Altair).
    • mercurius-apollo-registry: Reports schemas to Apollo Studio.
    • mercurius-apollo-tracing: Reports performance metrics and errors to Apollo Studio.
    • mercurius-postgraphile: Integrates PostGraphile schemas.
    • mercurius-logging: Enhances GraphQL request logging with query insights.
    • mercurius-fetch: Adds fetch capabilities to a REST API directly on queries or properties.
    • mercurius-hit-map: Counts resolver execution frequency.
  3. Understand the Subscription Context

    master

    The context object provided to subscription resolvers contains:

    • app: The Fastify application instance.
    • reply: A mock object behaving like a Fastify Reply object (without decorators).
    • reply.request: The actual Fastify Request object.

    Connection Initialization: During the connection_init phase, the contents of the payload property in the packet are automatically copied into request.headers. If the payload explicitly contains an headers property, that property is used instead.

  4. Implement the CustomPubSub interface

    master

    If you need complete control over the pub/sub mechanism, you can implement the CustomPubSub interface and pass it to the subscription.pubsub option.

    Note: If you provide both pubsub and emitter in the configuration, emitter will be ignored.

    To implement CustomPubSub, your class must provide:

    • subscribe(topic, queue, ...customArgs): Returns a Promise. The queue is a Readable stream where data is pushed. customArgs allows passing extra parameters (like offset) from the resolver.
    • publish(event, callback): event contains topic and payload. The callback is invoked when the operation completes.
    class CustomPubSub {
      constructor () {
        this.emitter = new EventEmitter()
      }
    
      async subscribe (topic, queue, offset) {
        const listener = (value) => {
          queue.push(value)
        }
    
        const close = () => {
          this.emitter.removeListener(topic, listener)
        }
    
        this.emitter.on(topic, listener)
        queue.close.push(close)
      }
    
      publish (event, callback) {
        this.emitter.emit(event.topic, event.payload)
        callback()
      }
    }
    
    const pubsub = new CustomPubSub()
    
    app.register(mercurius, {
      schema,
      resolvers: {
        Subscription: {
          retrieveItems: {
            subscribe: (root, args, { pubsub }) => pubsub.subscribe('RETRIEVE_ITEMS', args.offset)
          }
        }
      },
      subscription: {
        pubsub
      }
    })
  5. How GraphQL Request Hooks work

    master

    GraphQL Request Hooks allow you to intercept the lifecycle of a standard GraphQL query or mutation. They execute in the following order:

    1. preParsing: Access the raw GraphQL query string before parsing.
      • Arguments: (schema, source, context)
    2. preValidation: Triggered after the query is parsed into a GraphQL Document AST. This hook is not triggered for cached queries.
      • Arguments: (schema, document, context)
    3. preExecution: Triggered before execution. You can modify and return the following objects to affect the request:
      • document, schema, variables, errors.
      • Warning: Modifying schema or document will disable JIT compilation for that request.
      • Arguments: (schema, document, context, variables)
    4. onResolution: Runs after the query execution is complete.
      • Arguments: (execution, context)

    If you throw an error inside any request hook, Mercurius will automatically close the request and send the error to the user.

    // Example: preExecution modification
    fastify.graphql.addHook('preExecution', async (schema, document, context, variables) => {
      const { 
        modifiedSchema, 
        modifiedDocument, 
        modifiedVariables, 
        errors 
      } = await asyncMethod(document)
    
      return {
        schema: modifiedSchema,
        document: modifiedDocument,
        variables: modifiedVariables,
        errors
      }
    })
  6. Understand the Subscription Lifecycle

    master

    For WebSocket-based subscriptions, Mercurius follows a distinct lifecycle that manages the connection, parsing, execution, and the eventual end of the subscription.

    Key phases include:

    • preSubscriptionParsing Hook: Intercepts the process after routing but before parsing the subscription data.
    • preSubscriptionExecution Hook: Intercepts the process before subscription execution begins.
    • Subscription Resolution: Occurs when subscription data is received. This triggers the onSubscriptionResolution Hook.
    • Subscription End: Occurs when a stop signal is received. This triggers the onSubscriptionEnd Hook.
    • Connection Lifecycle Hooks: onSubscriptionConnectionClose Hook is called when the connection closes, and onSubscriptionConnectionError Hook is called if a connection error occurs.
    Incoming GraphQL Websocket subscription data
      │
      └─▶ Routing
               │
      errors ◀─┴─▶ preSubscriptionParsing Hook
                      │
             errors ◀─┴─▶ Subscription Parsing
                            │
                   errors ◀─┴─▶ preSubscriptionExecution Hook
                                  │
                         Subscription Execution
                                                  │
                                      wait for subscription data
                                                  │
                   subscription closed on error ◀─┴─▶ Subscription Resolution (when subscription data is received)
                                                          │
                                                          └─▶ onSubscriptionResolution Hook
                                                                │
                                                keeping processing until subscription ended
                                                                │
                                 subscription closed on error ◀─┴─▶ Subscription End (when subscription stop is received)
                                                                      └─▶ onSubscriptionEnd Hook
      │
      └─▶ Connection Close
          │
          └─▶ onSubscriptionConnectionClose Hook
      │
      └─▶ Connection Error
          │
          └─▶ onSubscriptionConnectionError Hook
  7. How custom directives work in Mercurius

    master

    A custom directive allows you to decorate parts of your GraphQL schema (fields, arguments, or types) to add reusable features or modify behavior. A custom directive implementation consists of two parts:

    1. Schema Definition: Using the directive keyword in your SDL to define the directive name, arguments, and valid locations (e.g., FIELD_DEFINITION).
    2. Transformer: A function that takes an existing executable schema and applies modifications to the schema and its resolvers.

    To implement a directive, you typically use mapSchema from @graphql-tools/utils to intercept field configurations and wrap their resolve functions with new logic.

    // 1. Schema Definition
    const schema = `
        directive @redact(find: String) on FIELD_DEFINITION
        
        type Document {
          excerpt: String! @redact(find: "email")
        }
    `;
    
    // 2. Transformer (Logic to wrap resolvers)
    const redactionSchemaTransformer = schema =>
      mapSchema(schema, {
        [MapperKind.FIELD]: fieldConfig => {
          // ... logic to modify fieldConfig.resolve
        },
      });
  8. Understand the GraphQL over WebSocket message structure

    master

    When using WebSockets for GraphQL operations (typically for subscription types), messages follow a specific structure. The protocol uses a type field to define the message intent and an optional payload for the data.

    An OperationMessage can also include extensions, which are used to implement custom logic on top of the standard specification.

    export interface OperationMessage {
      payload?: any;
      id?: string;
      type: string;
    
      extensions?: Array<OperationExtension>;
    }
    
    export interface OperationExtension {
      type: string;
      payload?: any;
    }
  9. Refresh federated schemas in Gateway mode

    master

    The Gateway can refresh its composed schema in two ways:

    1. Periodically (Polling)

    Set gateway.pollingInterval (in milliseconds) in the plugin configuration. The gateway will poll subgraphs for schema changes. If the schema hasn't changed, the cached version is reused.

    2. Programmatically

    Manually trigger a re-fetch by calling application.graphql.gateway.refresh(). This method returns the new schema if changes were found, or null if no changes occurred. If a new schema is returned, use application.graphql.replaceSchema(schema) to apply it.

    If using a schema registry, you can update a service's schema via application.graphql.gateway.serviceMap[serviceName].setSchema(newSchema) before calling refresh().

    // Programmatic refresh example
    setTimeout(async () => {
      const schema = await server.graphql.gateway.refresh()
    
      if (schema !== null) {
        server.graphql.replaceSchema(schema)
      }
    }, 10000)
  10. What are Loaders and how do they work?

    master

    A Loader is a utility designed to solve the GraphQL '1 + N query problem'. It works by registering a resolver that coalesces multiple individual requests into a single, bulk query. This allows you to fetch data for multiple objects in one operation.

    Loaders also provide built-in caching to prevent redundant data fetching across different parts of a GraphQL query.

    Loader Function Signature: loader(queries, context)

    • queries: An array of objects: { obj, params, info }
      • obj: The current object being resolved.
      • params: The GraphQL parameters (equivalent to the first two arguments of a standard resolver).
      • info: Additional execution information. Note: The info object is only available if caching is disabled.
    • context: The GraphQL context, which includes the reply object.
    const loaders = {
      Dog: {
        async owner (queries, { reply }) {
          return queries.map(({ obj, params }) => owners[obj.name])
        }
      }
    }
  11. Define a GraphiQL plugin component

    master

    A GraphiQL plugin is an object that defines how a new feature appears and behaves within the GraphiQL interface. It must contain three properties:

    • title: A string representing the plugin's name.
    • icon: A React component used for the toolbar icon.
    • content: A React component that implements the plugin's UI.

    To allow the plugin to intercept or modify data fetched by GraphiQL, you can export a function (e.g., parseFetchResponse) and reference it via the fetcherWrapper configuration key. This is necessary because GraphiQL typically injects fetched data directly into the viewer as a stringified format without providing direct access to the raw data object.

    import React from 'react'
    
    function Content() {
      return (
        <div style={{ maxWidth: '300px' }}>
          <div style={{ height: '100%' }}>This is a sample plugin</div
        </div
      )
    }
    
    function Icon() {
      return <p>P</p>
    }
    
    export function parseFetchResponse(data) {
      if (data) {
        // Perform logic like: storeDataSomewhere(data), addInfoToData(data), etc.
      }
      return data
    }
    
    export function graphiqlSamplePlugin(props) {
      return {
        title: props.title || 'GraphiQL Sample',
        icon: () => <Icon />,
        content: () => {
          return <Content {...props}/>
        }
      }
    }
    
    // Required for Mercurius integration
    export function umdPlugin(props) {
        return graphiqlSamplePlugin(props)
    }