looking for a little help on querying relational d...
# orm-help
h
looking for a little help on querying relational data using custom resolvers and prisma-binding.... I have the following:
Copy code
import { helmet } from '../core/utils/helmet'
import { getVehicle, newVehicle, updateVehicle } from './resolvers'

export default {
  Query: {
    getVehicle: helmet(getVehicle),
  },
  Mutation: {
    newVehicle: helmet(newVehicle),
    updateVehicle: helmet(updateVehicle),
  },
  Vehicle: {
    driver:  async ( { userID }, args, context, info) => {
      return context.db.query.driverProfile({ where: { userID } }, info)
    },
  },
}
Copy code
import { Token } from '../../core/classes' // [TO DO]: get rid of the pathing

export const updateVehicle = async (obj, args, context, info) => {
  try {
    const token = new Token(context.request.headers.authorization)
    await token.validate()
    return await context.db.mutation.updateVehicle({
      where: { vehicleID: args.vehicle.vehicleID },
      // data: args.vehicle },
      data: {
        driver: {
          connect: {
            userID: args.vehicle.driver.userID,
          },
        },
      },
      info,
    })
  } catch (error) {
    return error
  }
}
So that's the specific resolver, and the roll-up into
Query
with a custom type resolver for
driver
I am unable to return the nested driver information, however, despite being able to create the relationship and see it in my database
in the
Vehicle.driver
resolver,
userID
comes back as
null
, because the parent object coming through is the
Vehicle
which has no
userID
and so that resolver fails
This seems like a no-brainer issue... so I imagine that I am missing something simple
Thoughts?
l
What's the mutation, actual result, and desired result?
(It looks like you may need fragments and type resolvers, but I just want to make sure)
h
mutation:
Copy code
mutation {
  updateVehicle( vehicle: {
    vehicleID: "1", 
    driver: {
      userID: "5af31686a6803a91913d42e6"
    }
  }) {
    vehicleID
    driver {
      userID
    }
  }
}
actual result gives me an error because of that one resolver
but I should be able to subsequently query
Vehicle
and request driver info
I have a type resolver for driver up there
Copy code
Vehicle: {
    driver:  async ( { userID }, args, context, info) => {
      return context.db.query.driverProfile({ where: { userID } }, info)
    },
  },
and this is specifically what seems to be wrong
(
userID
is
null
)
l
Ah gotcha, yeah, it needs a fragment..
Copy code
Vehicle: {
    driver:  {
         fragment: `fragment getUserId on Vehicle { userID } `,
         resolve: async ( { userID }, args, context, info) => {
      return context.db.query.driverProfile({ where: { userID } }, info)
    },
  },
Make sure that you're wrapping your resolvers with
extractFragmentReplacements
from
prisma-binding
Here's my
resolvers/index
and then I pass
fragmentReplacements
into the Prisma constructor.
Copy code
const { extractFragmentReplacements } = require(`prisma-binding`)
const { fileLoader } = require("merge-graphql-schemas")
const path = require("path")

const resolvers = fileLoader(path.join(__dirname, "../entity/**/resolver.js"))

const fragmentReplacements = extractFragmentReplacements(resolvers)

module.exports = { resolvers, fragmentReplacements }
h
hmm -- but my
Vehicle
type doesn't have a
userID
-- just the driver which is associated with
Vehicle
does that matter in the above?
l
Then you can do.. fragment:
fragment getUserId on Vehicle { driver { userID } }
. You may be able to fragment:
fragment getUserId on Driver { userID }
but I'm not as fresh on my fragments as I should be 🙂
h
👍
thanks! I'll give it a shot!
l
Prisma's AirBNB has a ton of great fragment examples. https://github.com/prismagraphql/graphql-server-example
👌 1
h
hmm. When I do this:
Copy code
Vehicle: {
    driver: {
      fragment: `fragment getUserID on Vehicle { driver { userID } }`,
      resolve: async ( { userID }, args, context, info) => {
        console.log('>>>>>>>>>>>>>>>>> ', info)
        // context.db.query.driverProfile({ where: { userID } }, info)
      },
    },
  },
It still fails, and
info.fragments
shows as an empty object
and here's my Prisma instantiation:
Copy code
const server = new GraphQLServer({
  schema,
  context: (req) => ({
    ...req,
    db: new Prisma({
      fragmentReplacements,
      typeDefs: 'generated/prisma.graphql',
      endpoint: process.env.PRISMA_ENDPOINT,
      secret: process.env.MANAGEMENT_API_SECRET,
      debug: true,
    }),
  }),
})
If I log
fragmentReplacements
just before instantiating Prisma, I get something useful:
Copy code
FRAGMENT REPLACEMENTS:  { Vehicle:
   { driver:
      { kind: 'InlineFragment',
        typeCondition: [Object],
        selectionSet: [Object] } } }
@nilan -- you seem to be something of a guru with regards to fragments
the above also looks correct, per this link: https://www.prisma.io/forum/t/resolver-implementation/3043/17
So I am wondering if the issue is perhaps somewhere in my data model?
@lawjolla so I flipped some stuff around, and am now getting this error:
Copy code
Error: Variable "$_where" got invalid value {"currentVehicle":{"id":"cjiabbu18000z0922p2j2soay"}}; Field "currentVehicle" is not defined by type DriverProfileWhereUniqueInput.
and I updated the resolver to read:
Copy code
Vehicle: {
    driver: {
      fragment: `fragment VehicleID on Vehicle { id }`,
      resolve: async ( { id }, args, context, info) => {
        return context.db.query.driverProfile({ where: { currentVehicle: { id } } })
      },
    },
  },
though.. at this point, I wonder if the fragment is even necessary, because it's still coming in as empty in
info.fragments
, and
id
exists on the parent object being passed to the resolver
¯\_(ツ)_/¯
l
I'm taking stabs at this not seeing your data model and API schema makes it hard. The fact that
id
isn't coming back suggests there's a problem with the fragment parsing code
Try a very simple
Vehicle
query and see if
id
returns. If it doesn't, that's likely the issue
h
sorted a bit -- it's my relationship in the DB
l
Really? Interesting
h
so I can query
diverProfiles
but not
driverProfile
because it is not seeing the
currentVehicle
as unique
but i have it set up as a 1:1 relation
I think...
l
Did you import
Query.driverProfile
?
h
so I have something akin to this:
Copy code
type DriverProfile {
  id: ID! @unique
  userID: String! @unique
  currentVehicle: Vehicle
}

type Vehicle {
  id: ID! @unique
  vehicleID: String! @unique
  driver: DriverProfile
}
and what it tells me when I query
driverProfile
using a vehicle ID is that it's not unique.
but if I query
driverProfiles
I get an array with a single member
Can you define relations as
@unique
?
l
Not to my knowledge. Let me take a look..
h
Yeah, I thought that for a 1:1 relation, you just omitted any array brackets
l
You can query
DriverProfile
with userID and
Vehicle
with
vehicleId
separately, right?
h
yep!
l
And you only want one Vehicle to have one DriverProfile, and that DriveProfile only connected to that one Vehicle?
h
yep!
you got it
l
This doesn't help, but form a modeling standpoint, you may want to fold the types into one unless the separation is getting you something else
h
it is, unfortunately 😕
there are quite a number of other fields that I can't post
and there are other entities that vehicle and driver need to relate to, independently of each other
Now... I suppose I could maintain this relationship with an FKEY and not let Prisma set up a relation table.
...just store the driver ID on the vehicle as a unique, nullable value
l
You can get around the posting problem in a variety of ways. (I wrote an article on one of them 🙂 ). I just went through my various app's GraphQL schemas to look for a 1-1 relation and I can't find one. It makes sense in SQL, but I'm not sure it makes sense here.
h
but that feels like it runs counter to the whole idea of graphql (the FKEY thing)
yeah, i'm a lot more accustomed to REST/SQL
this is my first foray into GraphQL
l
I could be leading you astray because it's not a structure I've thought much about in GraphQL. If type A can only belong to type B, and B can only belong to A, what are the arguments for making two types?
h
A can belong to B or C.... and B can belong to C or D.... but only one A can belong to a B at a time, and vice versa
and they can couple/uncouple throughout app lifecycle
l
That can be done in the same type
h
oh yeah?
l
Ok, I'm starting to follow now..
Your problem (as I think you already stated, and I'm just figuring out now 🙂 ), is that Prisma doesn't link types in a singular query
But since it's a 1:1, it doesn't need to be queried
Copy code
query {
  Vehicle(where: { id: "a stored vehicle id"}) {
     driver { id }
  }
}
returns what?
h
null
, if I recall... but it's been a minute. confirming now
Copy code
query {
  getVehicle (vehicleID: "1") {
    vehicleID
    make
    model
    driver {
      userID
    }
  }
}
Returns the following:
Copy code
{
  "data": {
    "getVehicle": null
  },
  "errors": [
    {
      "message": "Cannot read property 'userID' of undefined",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "getVehicle"
      ]
    }
  ]
}
l
What if you just call for id?
h
and the resolver is:
Copy code
export const getVehicle = async (obj, args, context, info) => {
  try {
    const token = new Token(context.request.headers.authorization)
    await token.validate()
    return await context.db.query.vehicle(
      {
        where: args,
        data: {
          driver: {
            connect: {
              userID: args.driver.userID,
            },
          },
        },
      },
      info,
    )
  } catch (error) {
    return error
  }
}
lemme try for ID
hmm.
cannot query field 'id' on type 'DriverProfile'
l
You shouldn't have access to
data
under
db.query
because that's a
query.mutation
property
Your query is typically...
Copy code
return await context.db.query.vehicle(
      {
        where: { id: args.id },
      info
    )
h
ok - refactored:
Copy code
export default {
  Query: {
    getVehicle: async (obj, args, context, info) => {
      try {
        const token = new Token(context.request.headers.authorization)
        await token.validate()
        return await context.db.query.vehicle({  where: args }, info)
      } catch (error) {
        return error
      }
    },
  },

  Vehicle: {
    driver: {
      fragment: `fragment getUserID on DriverProfile { id }`,
      resolve: async ( { id }, args, context, info) => {
        return context.db.query.driverProfiles({ where: { id }  })
      },
    },
  },

}
returns
Copy code
{
  "data": {
    "getVehicle": {
      "vehicleID": "1",
      "driver": {
        "userID": null
      }
    }
  }
}
l
Ok, better! The Vehicle resolver is messing it up now. Try commenting it out and see what happens
h
success!!
awesome! 😄
so... do i need that vehicle resolver, though, for mutations?
l
❤️ progress
I don't think so with your models
h
awesome
just about every example I see involving relational data has those, though -- explain 'em like I'm 5?
when do you need them, and when not?
l
Sorry for my initial advice. I didn't understand it well enough on the first pass.
h
no worries -- you've been amazingly helpful, and I know this has taken a while 🙂
l
Have you read Nikolas's article on the
info
object?
h
I've not -- link?
The best article written that distills down a lot of what we talked about. Every time I reread it, I get even more out of it
h
Nice! I'll read it tonight -- thanks!
l
Absolutely!
h
so then in a mutation, if i want to pair a driver to a vehicle... that's when I use
connect
?
l
Exactly
Copy code
mutation {
  connectDriverAndVehicle(...): Vehicle
}
h
ok... so just something specific and single-use like that. it wouldn't be part of a broader
updateVehicle
mutation?
From what I'm seeing, best practice in GraphQL seems to be small, concise methods with intuitive method names... like writing JS... as opposed to REST's more broad, very resource-based pattern?
Is that accurate?
l
Yes, that's the preferred pattern over a generic
updateVehicle
type. My personal opinion is if others aren't consuming your API (i.e. just your client), the generic may be useful as a time saver. Otherwise, I'd make it specific
h
👍
again - thank you!
l
And for your particular case, you may have a fair amount of logic, so specific may be the right way still
No problem!
n
sorry, haven't read the entire thread but do you need more input, @hez?