Search code examples
javascriptgraphqlarangodbapollo-serverarangojs

Using dataloader for resolvers with nested data from ArangoDB


I'm implementing a GraphQL API over ArangoDB (with arangojs) and I want to know how to best implement dataloader (or similar) for this very basic use case.

I have 2 resolvers with DB queries shown below (both of these work), the first fetches Persons, the 2nd fetches a list of Record objects associated with a given Person (one to many). The association is made using ArangoDB's edge collections.

import { Database, aql } from 'arangojs'
import pick from 'lodash/pick'
const db = new Database('http://127.0.0.1:8529')
db.useBasicAuth('root', '')
db.useDatabase('_system')

// id is the auto-generated userId, which `_key` in Arango
const fetchPerson = id=> async (resolve, reject)=> {

    try {

        const cursor = await db.query(aql`RETURN DOCUMENT("PersonTable", ${String(id)})`)

        // Unwrap the results from the cursor object
        const result = await cursor.next()

        return resolve( pick(result, ['_key', 'firstName', 'lastName']) )

    } catch (err) {

        return reject( err )
    }

}

// id is the auto-generated userId (`_key` in Arango) who is associated with the records via the Person_HasMany_Records edge collection
const fetchRecords = id=> async (resolve, reject)=> {

    try {

        const edgeCollection = await db.collection('Person_HasMany_Records')

        // Query simply says: `get all connected nodes 1 step outward from origin node, in edgeCollection`
        const cursor = await db.query(aql`
            FOR record IN 1..1
            OUTBOUND DOCUMENT("PersonTable", ${String(id)})
            ${edgeCollection}
            RETURN record`)

        return resolve( cursor.map(each=>
            pick(each, ['_key', 'intro', 'title', 'misc']))
        )

    } catch (err) {

        return reject( err )
    }

}

export default {

    Query: {
        getPerson: (_, { id })=> new Promise(fetchPerson(id)),
        getRecords: (_, { ownerId })=> new Promise(fetchRecords(ownerId)),
    }

}

Now, if I want to fetch the Person data with the Records as nested data, in a single request, the query would be this:

aql`
LET person = DOCUMENT("PersonTable", ${String(id)})
LET records = (
  FOR record IN 1..1
  OUTBOUND person
  ${edgeCollection}
  RETURN record
)
RETURN MERGE(person, { records: records })`

So how should I update my API to employ batch requests / caching? Can I somehow run fetchRecords(id) inside of fetchPerson(id) but only when fetchPerson(id) is invoked with the records property included?

The setup file here, notice I'm using graphql-tools, because I took this from a tutorial somewhere.

import http from 'http'
import db from './database'

import schema from './schema'
import resolvers from './resolvers'

import express from 'express'
import bodyParser from 'body-parser'
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'
import { makeExecutableSchema } from 'graphql-tools'

const app = express()

// bodyParser is needed just for POST.
app.use('/graphql', bodyParser.json(), graphqlExpress({
    schema: makeExecutableSchema({ typeDefs: schema, resolvers })
}))
app.get('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })) // if you want GraphiQL enabled

app.listen(3000)

And here's the schema.

export default `
type Person {
    _key: String!
    firstName: String!
    lastName: String!
}

type Records {
    _key: String!
    intro: String!
    title: String!
    misc: String!
}

type Query {
    getPerson(id: Int!): Person
    getRecords(ownerId: Int!): [Record]!
}

type Schema {
    query: Query
}
`


Solution

  • I guess I was confused about the capability of dataloader. Serving nested data was really the stumbling block for me.

    This is the missing code. The export from resolvers.js needed a person property,

    export default {
    
        Person: {
            records: (person)=> new Promise(fetchRecords(person._key)),
        },
        Query: {
            getPerson: (_, { id })=> new Promise(fetchPerson(id)),
            getRecords: (_, { ownerId })=> new Promise(fetchRecords(ownerId)),
        },
    
    }
    

    And the Person type in the schema needed a records property.

    type Person {
        _key: String!
        firstName: String!
        lastName: String!
        records: [Records]!
    }
    

    Seems these features are provided by Apollo graphql-tools.