Apollo-server: Datasources missing in the context when using subscriptions

Created on 13 Aug 2018  路  21Comments  路  Source: apollographql/apollo-server

Intended outcome: Context in the resolver functions will contain dataSources field when using subscriptions just like when calling with queries.

Actual outcome: Field dataSources is missing while using subscriptions in the context (in subscribe and resolve functions). The context contains everything except this one field.

How to reproduce the issue:

1) set dataSources to the server

const server = new ApolloServer({
    schema: Schema,
    dataSources: () => createDataSources(),
    // ...
})

2) dump content of the context:

subscribe: (parent, args, context) => {
    console.warn(context); // dataSources missing, every other field is there correctly
})

I think this is quite serious because otherwise, I cannot query my data sources at all while using subscriptions.

馃К subscriptions

Most helpful comment

I solved this by using @PatrickStrz method with some little tweak to have each Datasource instance to have the full context, Same way apollo server initializes its Datasources

const pubSub = new PubSub();

/**
 * Generate dataSources for both transport protocol
 */
function dataSources() {
  return {
    userService: new UserService(),
    ministryService: new MinistryService(),
    mailService: new MailService(),
  };
}

/**
 * Authenticate  subscribers on initial connect
 * @param {*} connectionParams 
 */
async function onWebsocketConnect(connectionParams) {
  const authUser = await authProvider.getAuthenticatedUser({
    connectionParams,
  });
  if (authUser) {
    return { authUser, pubSub, dataSources: dataSources() };
  }
  throw new Error("Invalid Credentials");
}

/**
 * Initialize subscribtion datasources
 * @param {Context} context 
 */
function initializeSubscriptionDataSources(context) {
  const dataSources = context.dataSources;
  if (dataSources) {
    for (const instance in dataSources) {
      dataSources[instance].initialize({ context, cache: undefined });
    }
  }
}

/**
 * Constructs the context for transport (http and ws-subscription) protocols
 */
async function context({ req, connection }) {
  if (connection) {
    const subscriptionContext = connection.context;
    initializeSubscriptionDataSources(subscriptionContext);
    return subscriptionContext;
  }

  const authUser = await authProvider.getAuthenticatedUser({ req });
  return { authUser, pubSub };
}

/**
 * Merges other files schema and resolvers to a whole
 */
const schema = makeExecutableSchema({
  typeDefs: [rootTypeDefs, userTypeDefs, ministryTypeDefs, mailTypeDefs],
  resolvers: [rootResolvers, userResolvers, ministryResolvers, mailResolvers],
});

/**
 * GraphQL server config
 */
const graphQLServer = new ApolloServer({
  schema,
  context,
  dataSources,
  subscriptions: {
    onConnect: onWebsocketConnect,
  },
});


Hope this helps :)

All 21 comments

Unfortunately, this is a known issue. Although we run SubscriptionServer as part of ApolloServer, the request pipeline is completely separate. That means features like persisted queries, data sources, and tracing don't currently work with subscriptions (or queries and mutations over the WebSocket transport).

There is work underway on a refactoring of the apollo-server-core request pipeline that will make it transport independent, and that will allow us to build the WebSocket transport on top of it. Until then, I'm afraid there isn't much we can do.

Hi, did anyone come up with a solution to this?

I am currently adding my dataSources to both the dataSource and context methods, but this feels kind of icky...

const LanguagesAPI = require('./dataSources/LanguagesAPI')

const server = new ApolloServer({
  ...
  dataSources: () => {
    return: {
      LanguagesAPI: new LanguagesAPI(),
    }
  },
  context: ({req, connection}) => {
    if (connection) {
      return {
        dataSources: {
          LanguagesAPI: new LanguagesAPI(),
       }
    }
    return {
      // req context stuff
    }
  }
}

any update on this?

Any update? Can't use data sources as newing one up leaves it uninitialised..

also just ran into this issue and curious about solutions/fixes (update 5/4/2019 will wait until apollo 3.0, plenty of workarounds for now, helpful to understand)

I use the method @BrockReece provided but then got the error:
"Please use the dataSources config option instead of putting dataSources on the context yourself."

We can follow the 3.0 Roadmap, which includes "Unify diverging request pipelines":

https://github.com/apollographql/apollo-server/issues/2360

I guess it is related to #2561 as well.

I solved this by using @BrockReece's method but initializing DataSource classes manually:

const constructDataSourcesForSubscriptions = (context) => {
  const initializeDataSource = (dataSourceClass) => {
    const instance = new dataSourceClass()
    instance.initialize({ context, cache: undefined })
    return instance 
  }

  const LanguagesAPI = initializeDataSource(LanguagesAPI)

  return {
    LanguagesAPI,
  }
}

const server = new ApolloServer({
  ...
  dataSources: () => {
    return: {
      LanguagesAPI: new LanguagesAPI(),
    }
  },
  context: ({req, connection}) => {
    if (connection) {
      return {
        dataSources: constructDataSourcesForSubscriptions(connection.context)
    }
    return {
      // req context stuff
    }
  }
}

Hope this helps :)

This is indeed on the near term roadmap! https://github.com/apollographql/apollo-server/issues/1526#issuecomment-489306697

@jbaxleyiii so why is this ticket closed? Is 3.0 out?

Any update on this?

any update on this?

It's 2020 now... Any update on this?

Would be great if that could be solved somehow. Still have the same problem.

I solved this by using @PatrickStrz method with some little tweak to have each Datasource instance to have the full context, Same way apollo server initializes its Datasources

const pubSub = new PubSub();

/**
 * Generate dataSources for both transport protocol
 */
function dataSources() {
  return {
    userService: new UserService(),
    ministryService: new MinistryService(),
    mailService: new MailService(),
  };
}

/**
 * Authenticate  subscribers on initial connect
 * @param {*} connectionParams 
 */
async function onWebsocketConnect(connectionParams) {
  const authUser = await authProvider.getAuthenticatedUser({
    connectionParams,
  });
  if (authUser) {
    return { authUser, pubSub, dataSources: dataSources() };
  }
  throw new Error("Invalid Credentials");
}

/**
 * Initialize subscribtion datasources
 * @param {Context} context 
 */
function initializeSubscriptionDataSources(context) {
  const dataSources = context.dataSources;
  if (dataSources) {
    for (const instance in dataSources) {
      dataSources[instance].initialize({ context, cache: undefined });
    }
  }
}

/**
 * Constructs the context for transport (http and ws-subscription) protocols
 */
async function context({ req, connection }) {
  if (connection) {
    const subscriptionContext = connection.context;
    initializeSubscriptionDataSources(subscriptionContext);
    return subscriptionContext;
  }

  const authUser = await authProvider.getAuthenticatedUser({ req });
  return { authUser, pubSub };
}

/**
 * Merges other files schema and resolvers to a whole
 */
const schema = makeExecutableSchema({
  typeDefs: [rootTypeDefs, userTypeDefs, ministryTypeDefs, mailTypeDefs],
  resolvers: [rootResolvers, userResolvers, ministryResolvers, mailResolvers],
});

/**
 * GraphQL server config
 */
const graphQLServer = new ApolloServer({
  schema,
  context,
  dataSources,
  subscriptions: {
    onConnect: onWebsocketConnect,
  },
});


Hope this helps :)

@josedache Awesome, works like a charm! Thank you!

Glad to help @IsmAbd

It's quite an old issue now but I want to share with you my solution :)

It's very simple and looks like the previous solutions. I rename my createContext function into createBaseContext and add the data sources initialization into a new createContext function. No code duplication and easy to maintain/update

import { PrismaClient } from '@prisma/client'
import { PubSub } from 'apollo-server'
import { Request, Response } from 'express'
import { ExecutionParams } from 'subscriptions-transport-ws'
import { Auth } from './auth'
import { createDataSources, DataSources } from './data-sources'
import { getUserId } from './utils'

const prisma = new PrismaClient()
const pubSub = new PubSub()

export interface ExpressContext {
  req: Request
  res: Response
  connection?: ExecutionParams
}

// You can put whatever you like into this (as you can see I personnally use prisma, pubSub and an homemade Auth API)
export interface BaseContext extends ExpressContext {
  prisma: PrismaClient
  pubSub: PubSub
  auth: Auth
}

export type Context = BaseContext & { dataSources: DataSources }

export function createBaseContext(context: ExpressContext): BaseContext {
  const userId = getUserId(context)
  return {
    ...context,
    // You can put whatever you like into this
    prisma,
    pubSub,
    auth: new Auth(userId),
  }
}

export function createContext(context: ExpressContext): Context {
  const base = createBaseContext(context) as Context
  if (context.connection) {
    base.dataSources = createDataSources()
  }
  return base
}

Just want to note/summarize that

  • @BrockReece's example wouldn't ever call initialize on the DataSource instances, which might be problematic for some classes of DataSource.
  • @PatrickStrz's and @josedache's examples do call initialize, but with an undefined cache property, which is problematic (more following)
  • @MikaStark's example doesn't contain the code for their createDataSources function, so unsure whether it calls initialize methods with a cache.

My impression is that the cache property on DataSourceConfig is _the_ way multiple DataSource instances are intended to share state. For example, the one Apollo-supported DataSource, RESTDataSource, uses the cache as the backing store for its HTTP request/response cache (it namespaces its entries with the prefix httpcache:). When initialized with cache: undefined, RESTDataSource instances will all use instance-specific InMemoryLRUCaches. Since you typically instantiate new instances for every GraphQL operation, that means multiple operations don't share a cache. That might not be what you want.

I suspect most people will instead want to explicitly pass a cache to the Apollo Server config (which the framework then initializes query + mutation datasources with), and also pass that same object to the manual initialize DataSource calls done for subscriptions. Then every operation the server handles, including subscriptions, uses the same cache.

For reference, the default is (src) a new InMemoryLRUCache() from apollo-server-caching.

Just worth noting that the subscriptions integration in Apollo Server has always been incredibly superficial, and we are planning to remove it in Apollo Server 3 (replacing it with instructions on how to use a more maintained subscriptions package alongside Apollo Server). We do hope to have a more deeply integrated subscriptions implementation at some point but the current version promises more than it delivers, as this and many other issues indicate.

Was this page helpful?
0 / 5 - 0 ratings