Conditional branches within graphQL resolve - graphql

I have a graphQL RootQueryType which currently looks like this:
const RootQueryType = new graphql.GraphQLObjectType({
name: 'RootQueryType',
fields: {
accounts: {
type: graphql.GraphQLList(Account),
description: 'A list of accounts matching the provided arguments.',
args: {
type: {
type: graphql.GraphQLString
},
country: {
type: graphql.GraphQLString
},
currency: {
type: graphql.GraphQLString
}
},
resolve: (obj, args) => {
if (args.country === 'US') {
db.USaccounts.query()
} else if (args.currency === 'GBP') {
db.UKaccounts.query()
} else if ...
}
}
}
});
Basically, my resolve will be a large set of conditional branches which check for the presence of various combinations of arguments and call the appropriate DB or internal API endpoint with the correct args passed. Is this the correct approach to handling arguments in graphQL?

Related

Is it possible to do a multi tenancy with Graphql and Sequelize?

I have a rather tricky question about GraphQl and multi-tenancy.
Let's assume there are 3 tables, OWNER, HOUSE and TENANTS. I will describe them in Sequelize and GraphQl pseudo code:
Owner table (has multiple houses and multiple tenants)
const OWNER = sequelize.define('owner', {
ownerId: type: Sequelize.INTEGER,
name: type: Sequelize.STRING
}
OWNER.associate = models => {
models.owner.hasMany(models.house, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
models.owner.hasMany(models.tenant, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
}
House table (belongs to owner and has multiple tenants)
const HOUSE = sequelize.define('house', {
houseId: type: Sequelize.INTEGER,
ownerId: type: Sequelize.INTEGER,
name: type: Sequelize.STRING
}
HOUSE.associate = models => {
models.house.belongsTo(models.owner, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
models.house.hasMany(models.tenant, {foreignKey: {name: 'houseId', field: 'houseId'}})
}
Tenant table (belongs to owner and house)
const TENANT = sequelize.define('tenant', {
tenantId: type: Sequelize.INTEGER,
ownerId: type: Sequelize.INTEGER,
houseId: type: Sequelize.INTEGER,
name: type: Sequelize.STRING
}
TENANT.associate = models => {
models.tenant.belongsTo(models.owner, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
models.tenant.belongsTo(models.house, {foreignKey: {name: 'houseId', field: 'houseId'}})
}
The owner graphql object
const OwnerType = new GraphQLObjectType({
name: 'Owner',
fields: () => ({
ownerId: { type: GraphQLInt },
name: { type: GraphQLString },
houses: {
type: GraphQLList(HouseType),
resolve(owner) {
return owner.getHouse()
}
},
houseById: {
type: HouseType,
args: <args is not defined>
resolve(owner) {
return <???>
}
},
})
})
Here are a few simple GraphQL queries:
ownerById = {
type: OwnerType,
args: {
ownerId: { type: GraphQLInt },
},
resolve(parents, args){
return models.owner.findOne({ where: args })
}
}
houses = {
type: GraphQLList(HouseType),
resolve(parents, args){
return models.house.findAll()
}
}
houseById = {
type: HouseType,
args: {
houseId: { type: GraphQLInt },
},
resolve(parents, args){
return models.house.findOne({ where: args })
}
}
tenants = {
type: GraphQLList(TenantType),
resolve(parents, args){
return models.tenant.findAll()
}
}
These client queries work:
{
ownerById(ownerId: 1) {
ownerId
name
house {
houseId
name
}
}
}
{
houseById(houseId: 2) {
houseId
name
tenant {
tenantId
name
}
}
}
What I need to make multi-tenancy to work is something like that:
{
ownerById(ownerId: 1) {
ownerId
name
houseById(houseId: 2) {
houseId
name
tenant {
tenantId
name
}
}
}
}
Is there a way to archive this or is that out of scope what GraphQl can do?
If yes, how would the graphql object houseById query look like?
Thanks in advance.
Unless I'm missing something, it seems like your resolver for houseById would not be that different from the resolver for the houses field on the same type.
houseById: {
type: HouseType,
args: {
houseId: { type: GraphQLInt },
},
async resolve(owner, { houseId }) {
const houses = await owner.getHouses({ where: { id: houseId } })
return houses[0]
}
},
For a HasMany association, the getter for the target model resolves to an array of instances. So we need to grab that array first and then return just the first item in it, since our field represents a single object and not a list. If you don't want to use async/await, you can also do:
return owner.getHouses({ where: { id: houseId } })
.then(houses => houses[0])
It would also be worth mentioning that this sort of pattern for a schema defies convention. Rather than having a houses field, a houseById field, a houseBySomeOtherArg field, etc., consider exposing a single houses field with one or more arguments like id, name or whatever filter criteria you want to provide. Your field can then just filter the houses based on whatever arguments are passed in, or return all results if no filter arguments were provided.

GraphQL Nested Data in Mutation

I am having some difficulty getting a mutation working in GraphQL where the type in the schema includes a nested type. So say I have a data type for a booking:
const BookingType = new GraphQLObjectType({
name: 'Booking',
fields: () => ({
id: { type: GraphQLInt },
Date: { type: GraphQLString },
Venue: { type: GraphQLString }
})
});
In the schema file I also have a root mutation which looks like this:
createBooking: {
type: BookingType,
args: {
Date: { type: new GraphQLNonNull(GraphQLString) },
Venue: { type: new GraphQLNonNull(GraphQLString) }
},
resolve(parentValue, args){
return axios.post('http://localhost:3000/booking', args)
.then(resp => resp.data);
}
}
I can write a mutation in GraphiQL to create data for the booking no problem:
mutation {
createBooking(
Date: "2018-03-12",
Venue: "Some place",
) {
id
Date
Venue
}
}
So far so good. Now, I need to add a nested type to the original booking object to record staff members assigned to the booking. So I added types for the staff member (both input and output types) and added those to the Booking type and the mutation:
// output type
const AssignedStaffType = new GraphQLObjectType({
name: 'AssignedStaff',
fields: () => ({
id: { type: GraphQLInt },
Name: { type: GraphQLString }
})
});
// input type
const AssignedStaffInputType = new GraphQLInputObjectType({
name: 'AssignedStaffInput',
fields: () => ({
id: { type: GraphQLInt },
Name: { type: GraphQLString }
})
});
The booking type becomes:
const BookingType = new GraphQLObjectType({
name: 'Booking',
fields: () => ({
id: { type: GraphQLInt },
Date: { type: GraphQLString },
Venue: { type: GraphQLString },
Staff: { type: new GraphQLList(AssignedStaffType) }
})
});
And the root mutation becomes:
createBooking: {
type: BookingType,
args: {
Date: { type: new GraphQLNonNull(GraphQLString) },
Venue: { type: new GraphQLNonNull(GraphQLString) },
Staff: { type: new GraphQLList(AssignedStaffInputType) }
},
resolve(parentValue, args){
return axios.post('http://localhost:3000/booking', args)
.then(resp => resp.data);
}
}
What I don't know is how to now formulate the mutation in GraphiQL, specifically what to use as a value for Staff:
mutation {
createBooking(
Date: "2018-03-14",
Venue: "Some place",
Staff: // ??? <--- What goes here??
) {
id
Venue
Date
Staff
}
}
I have tried giving it an object, or an array of objects which have the same structure as AssignedStaffInputType, but I just get an error ('expecting AssignedStaffInputType'). The client (GraphiQL in this instance) doesn't know anything about the AssignedStaffInputType as defined in the schema, so I don't understand a) how to use this input type in the client, or b) how I would then populate such a type with the required data.
Help please!
Never mind, I figured it out. I can, in fact, pass an object (or array of objects) in the correct format (specified in the input type in the schema) and it works fine. The reason I was having problems is that I had the wrong scalar type for one of the fields in the input type and this was throwing the error. The client doesn't need to know about the types specified in the schema it seems. So, the above problematic mutation should, in fact, be written like this:
mutation {
createBooking(
Date: "2018-03-14",
Venue: "Some place",
Staff: [{staffId: 1}]
) {
id
Venue
Date
Staff{
Name
}
}
}

How can GraphQL enable an ID based query at sub fields level?

If an existing service supporting the following GraphQL queries respectively:
query to a person's bank account:
query {
balance(id: "1") {
checking
saving
}
}
result
{
"data": {
"balance": {
"checking": "800",
"saving": "3000"
}
}
}
query to a person's pending order:
query {
pending_order(id: "1") {
books
tickets
}
}
result
{
"data": {
"pending_order": {
"books": "5",
"tickets": "2"
}
}
}
The source code achieving the above functionality is something like this:
module.exports = new GraphQLObjectType({
name: 'Query',
description: 'Queries individual fields by ID',
fields: () => ({
balance: {
type: BalanceType,
description: 'Get balance',
args: {
id: {
description: 'id of the person',
type: GraphQLString
}
},
resolve: (root, { id }) => getBalance(id)
},
pending_order: {
type: OrderType,
description: 'Get the pending orders',
args: {
id: {
description: 'id of the person',
type: GraphQLString
}
},
resolve: (root, { id }) => getPendingOrders(id)
}
})
});
Now, I want to make my GraphQL service schema support person level schema, i.e.,
query {
person (id: "1") {
balance
pending_order
}
}
and get the following results:
{
"data": {
"balance": {
"checking": "800",
"saving": "3000"
}
"pending_order": {
"books": "5",
"tickets": "2"
}
}
}
How can I re-structure the schema, and how can I reuse the existing query service?
EDIT (after reading Daniel Rearden's answer):
Can we optimize the GraphQL service so that we make service call based upon the query? i.e., if the incoming query is
query {
person (id: "1") {
pending_order
}
}
my actually query becomes
person: {
...
resolve: (root, { id }) => Promise.all([
getBalance(id)
]) => ({ balance})
}
You're going to have to define a separate Person type to wrap the balance and pending_order fields.
module.exports = new GraphQLObjectType({
name: 'Person',
fields: () => ({
balance: {
type: BalanceType,
resolve: ({ id }) => getBalance(id)
},
pending_order: {
type: OrderType,
resolve: ({ id }) => getPendingOrders(id)
}
})
});
And you're going to need to add a new field to your Query type:
person: {
type: PersonType,
args: {
id: {
type: GraphQLString
}
},
// We just need to return an object with the id, the resolvers for
// our Person type fields will do the result
resolve: (root, { id }) => ({ id })
}
There's not much you can do to keep things more DRY and reuse your existing code. If you're looking for a way to reduce boilerplate, I would suggest using graphql-tools.

Filters in GraphQL

const { connectionType: PersonConnection } = connectionDefinitions({
name: 'Person',
nodeType: PersonType,
here i am using connectionFields for count
connectionFields: {
count: {
type: GraphQLInt,
resolve: (args) => {
const filter = args.args || {};
return Person.count(filter).exec();
},
},
},
});
i am quite confused about using args with custom filters and obtain data from database, using filter
if i don't provide any id count should provide all data count, if i provide any id it may also look for references data and search in another models so how to perform the count and efficient filteration of data.
Thanks in Advance
person: {
type: PersonConnection,
args: _.assign({
_id: { type: GraphQLID },
// assign mine custom filters
name: { type: GraphQLString },
location: { type: GraphQLString },
education: { type: GraphQLString },
}, connectionArgs),
resolve: (obj, args, auth, fieldASTs) => {
const filter = args;
return connectionFromPromisedArray(getPersons(filter, fieldASTs), args).then((data) => {
// using to connection Fields
data.args = filter;
return data;
}).catch(err => new Error(err));
},
},

GraphQL mutation without sub section

I want to send graphql mutation request without sub section
mutation _ {
updateCurrentUser(fullName: "Syava", email: "fake#gmail.com")
}
and I am getting
{
"errors": [
{
"message": "Field \"updateCurrentUser\" of type \"User\" must have a sub selection.",
...
}
]
}
add { id } to request works fine but I don't want
Also Schema code
const userType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLString) },
fullName: { type: GraphQLString },
email: { type: GraphQLString },
}),
});
type: userType,
args: {
fullName: { type: GraphQLString },
email: { type: new GraphQLNonNull(emailType) },
password: { type: GraphQLString },
},
resolve: async (root, { fullName, email, password }, { rootValue }) => {
const user = await User.findById(rootValue.req.user.id);
...
return user;
},
You define the type of the field to be UserType. Even though it's a mutation, it still follows the same rules and behavior as a query. Because UserType is an object type, it requires nested fields.
mutation _ {
updateCurrentUser(fullName: "Syava", email: "fake#gmail.com") {
fullName
email
}
}
// would respond with { fullName: 'Syava', email: 'fake#gmail.com' }
If you don't want the mutation to return a User, you can declare its type to GraphQLBoolean for example -- that's a scalar and doesn't have any nested fields.
{
type: GraphQLBoolean,
args: {
fullName: { type: GraphQLString },
email: { type: new GraphQLNonNull(emailType) },
password: { type: GraphQLString },
},
resolve: async (root, { fullName, email, password }, { rootValue }) => {
const user = await User.findById(rootValue.req.user.id);
user.fullName = fullName;
user.password = password; // or hashed to not store plain text passwords
return user.save(); // assuming save returns boolean; depends on the library you use
}
}
Note that the best practice for mutations in GraphQL APIs is to return a "result" object with multiple fields, such as the mutated object itself (e.g. user), clientMutationId (per Relay spec), and others as needed. This makes it flexible so you could add more data in the future.
updateCurrentUser(fullName: "Syava", email: "fake#gmail.com") {
clientMutationId
user {
...
}
}

Resources