Usando tarefas pg-promessa com graphql e dataloader

Sep 22 2020

A primeira coisa é dizer que vi esta pergunta Compartilhando uma tarefa pg-promessa em partes de uma solicitação http e li a resposta. Eu gostaria de revisitar a questão com algumas complicações adicionais.

Estou trabalhando em um servidor Graphql de typescript apollo-server-expressusando um banco de dados postgres como back-end e pg-promisecomo biblioteca de interface de banco de dados. Tenho resolvedores de consulta usando dataloaders usando o padrão descrito aqui:https://github.com/graphql/dataloader#creating-a-new-dataloader-per-request.

Todo o acesso ao banco de dados ocorre por meio de uma única instância, pg-promiseconforme recomendado, por exemplohttps://github.com/vitaly-t/pg-promise-demo/blob/master/TypeScript/db/index.ts.

Como a pergunta original, estou tentando encontrar uma boa maneira de criar uma tarefa que possa ser incorporada aos carregadores de dados (ou passada como um parâmetro quando o carregador de dados é chamado) de forma que os resolvedores de consultas operem em uma única conexão de banco de dados. Não sei quais resolvedores serão chamados ou em que ordem serão chamados, então não posso dizer que um determinado resolvedor deve criar a tarefa.

Tenho tentado descobrir uma maneira de usar o graphql Contextpara compartilhar a tarefa, pois estou usando o Contextpara compartilhar o objeto dataloaders, mas como foi observado, todas as chamadas na tarefa acontecem dentro de uma função de retorno de chamada, portanto, compartilhar a tarefa por meio do Contexté Fora. Aqui está um exemplo de como o contexto é configurado:

/*
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 }
  }
})

e um exemplo nos documentos da Apollo de como criar uma conexão db dentro do contexto: https://www.apollographql.com/docs/apollo-server/data/resolvers/#the-context-argument (note que eles não descrevem o fechamento da conexão em qualquer lugar).

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

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

Isso pode não ser possível sem o uso do connectmétodo, mas seu uso para este tipo de operação é desencorajado pelo autor da biblioteca. E surge o mesmo problema de que a conexão precisa ser retornada ao pool quando a consulta for finalmente resolvida, pouco antes de enviar os resultados de volta ao cliente.

Qualquer ajuda é apreciada! (e obrigado a @vitaly-t pelo forte trabalho em pg-promise)

Edite 1 @Daniel-Rearden sua sugestão e a essência do link acabaram funcionando. Provavelmente irei inspecionar as solicitações recebidas de dentro do plug-in se sempre abrir uma tarefa se tornar um problema. Marcando sua resposta correta!

Respostas

1 DanielRearden Sep 22 2020 at 10:02

O fato de você estar usando o DataLoader aqui é amplamente irrelevante. Como você já sugeriu, você só precisa passar qualquer instância que estiver usando para chamar o querymétodo createLoaders. Se você não estivesse usando o DataLoader, ainda ficaria com a mesma pergunta, ou seja, como criamos uma tarefa e a disponibilizamos para todos os nossos resolvedores.

Pelo que eu sei, a única maneira de fazer isso com o Apollo Server é criar um plugin personalizado. Você pode verificar esta essência que demonstrou o mesmo princípio, exceto com as transações. Efetivamente, você faria algo como:

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
            }
          })
      }
    }
  }
})

Instale seu plug-in conforme mostrado aqui na documentação :

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

Então, dentro de seu resolvedor, você faria algo como:

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