SFri
SFri

Reputation: 13

Using pg-promise Tasks with graphql and dataloader

First thing is to say that I have seen this question Sharing a pg-promise task across parts of an http request and read the answer. I'd like to revisit the question with some added complication.

I am working on a typescript graphql server on apollo-server-express using a postgres database as the backend and pg-promise as the database interface library. I have query resolvers using dataloaders using the pattern described here: https://github.com/graphql/dataloader#creating-a-new-dataloader-per-request.

All database access occurs via a single instance of pg-promise as recommended e.g. https://github.com/vitaly-t/pg-promise-demo/blob/master/TypeScript/db/index.ts.

Like the original question, I am trying to find a good way to create a Task that can be incorporated into the dataloaders (or passed as a param when the dataloader is called) such that query resolvers operate within a single database connection. I don't know which resolvers will be called or in which order they will be called so I can't say that a certain resolver should create the task.

I've been trying to figure a way to use the graphql Context to share the task as I am currently using the Context to share the dataloaders object but as was noted, all calls in the task happen within a callback function so sharing the task via the Context is out. Here's an example of how the context is setup:

/*
createLoaders() returns a new object of DataLoaders e.g.
{
  users: new DataLoader(ids => getUsers(ids)),
  tags: new DataLoader(ids => getTags(ids))
}
*/
import { createLoaders } from './dataloaders'

const server = new ApolloServer({
  schema,
  context: (): Promise<Context> => {
    // create a connection here to inject into createLoaders?
    const loaders = createLoaders()
    return { loaders }
  }
})

and an example in the Apollo docs of creating a db connection within the context: https://www.apollographql.com/docs/apollo-server/data/resolvers/#the-context-argument (note that they don't describe closing the connection anywhere).

context: async () => ({
  db: await client.connect(),
})

// Resolver
(parent, args, context, info) => {
  return context.db.query('SELECT * FROM table_name');
}

This may not be possible without using the connect method, but its use for this type of operation is discouraged by the author of the libary. And the same problem arises that the connection needs to be returned to the pool when the query is finally resolved just before sending results back to the client.

Any help is appreciated! (and thanks to @vitaly-t for such strong work on pg-promise)

Edit 1 @Daniel-Rearden your suggestion and linked gist ended up working. I will likely inspect incoming requests from within the plugin if always opening a task becomes an issue. Marking your answer correct!

Upvotes: 1

Views: 503

Answers (1)

Daniel Rearden
Daniel Rearden

Reputation: 84697

The fact that you're using DataLoader here is largely irrelevant. Like you already suggested, you just need to pass whatever instance you're using to call the query method to createLoaders. If you weren't using DataLoader, you would still be left with the same question, namely, how do we create a Task and make it available to all our resolvers.

As far as I'm aware, the only way to do that with Apollo Server is to create a custom plugin. You can check out this gist which demonstrated the same principle except with transactions. Effectively, you'd do something like:

import { ApolloServerPlugin, GraphQLRequestContext } from 'apollo-server-plugin-base'

export const createTaskPlugin = (db: Database): ApolloServerPlugin => ({
  requestDidStart(requestContext: GraphQLRequestContext) {
    let taskResult: Promise<{ ok?: boolean, error?: any }> | void = undefined
    return {
      executionDidStart() {
        let ok: (value?: unknown) => void
        let fail: (reason?: any) => void
        const didFinish = new Promise((resolve, reject) => {
          ok = resolve
          fail = reject
        })

        const task = new Promise((resolve) =>
          taskResult = db.task(t => {
            resolve(t)

            return didFinish
          }).then(() => ({ ok: true }), error => ({ error }))
        )

        requestContext.context.runWithTask = async (cb) => cb(await task)

        return (err) => {
          if (err) fail(err)
          ok()
        }
      },

      willSendResponse() {
        if (!taskResult) return

        return taskResult
          .then(({ error }) => {
            if (error) {
              throw error
            }
          })
      }
    }
  }
})

Install your plugin as shown here in the docs:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    createTaskPlugin(db),
  ],
})

Then inside your resolver, you would do something like:

resolve (parent, args, context) {
  return runWithTask(t => t.query('SELECT * FROM table_name'))
}

Upvotes: 1

Related Questions