GraphQL, Dataloader, [ORM or not], hasMany relationship understanding - orm

I'm using for the first time Facebook's dataloader (https://github.com/facebook/dataloader).
What I don't understand is how to use it when I have 1 to many relationships.
Here it is a reproduction of my problem: https://enshrined-hydrant.glitch.me.
If you use this query in the Playground:
query {
persons {
name
bestFriend {
name
}
opponents {
name
}
}
}
you get values.
But if you open the console log here: https://glitch.com/edit/#!/enshrined-hydrant you can see these database calls I want to avoid:
My Person type is:
type Person {
id: ID!
name: String!
bestFriend: Person
opponents: [Person]
}
I can use dataloader good for bestFriend: Person but I don't understand how to use it with opponents: [Person].
As you can see the resolver has to return an array of values.
Have you any hint about this?

You need to create batched endpoints to work with dataloader - it can't do batching by itself.
For example, you probably want the following endpoints:
GET /persons - returns all people
POST /bestFriends, Array<personId>` - returns an array of best friends matchin the corresponding array of `personId`s
Then, your dataloaders can look like:
function batchedBestFriends(personIds) {
return fetch('/bestFriends', {
method: 'POST',
body: JSON.stringify(personIds))
}).then(response => response.json());
// We assume above that the API returns a straight array of the data.
// If the data was keyed, you could add another accessor such as
// .then(data => data.bestFriends)
}
// The `keys` here will just be the accumulated list of `personId`s from the `load` call in the resolver
const bestFriendLoader = new DataLoader(keys => batchedBestFriends(keys));
Now, your resolver will look something like:
const PersonType = new GraphQLObjectType({
...
bestFriend: {
type: BestFriendType,
resolve: (person, args, context) => {
return bestFriendLoader.load(person.id);
}
}
});

Related

Calling function in VueApollo after API response

I am using Vue and Apollo and I am making a querie that looks just like the box below.
After I get the API response, I would like to call a method from my methods object. However Vue, doesn't give me acess to it within apollo object.
I would like to know how can I call one of my methods, but only after I am sure I got that response, without having to manually trigger it with a button or something else.
apollo: {
materials: {
query: gql`
query allMaterials($tenantId: ID, $name: String) {
tenantMaterials(tenantId: $tenantId, name: $name) {
edges {
node {
name
materialType {
name
id
}
brand
vendor
size
unit
inventory
createdAt
updatedAt
isActive
updatedBy
id
}
}
totalCount
}
}
`,
variables() {
return {
name: null
};
},
fetchPolicy: "cache-and-network",
update: response => {
return response.tenantMaterials.edges;
//I want to call a function/method after this response
},
skip: false
},
}
Use update(data) or result(result, key)
update(data) {return ...} to customize the value that is set in the
vue property, for example if the field names don't match.
result(ApolloQueryResult, key) is a hook called when a result is
received (see documentation for ApolloQueryResult (opens new window)).
key is the query key in the apollo option.
https://apollo.vuejs.org/api/smart-query.html

How to sort result set using computed fields (type-graphql)

I'm using type-graphql and typeorm. Is there a way to sort the result based on a computed field. Specifically, I want to return a list of Clients and sort the list based on a computed field: "sortName". Sort name is simply a string of the clent's "firstName lastName" or "lastName FirstName". The decision on how to generate the sortName is based on a flag in the Company table (that way the user can control how they want to view their clients). I just don't know how to do the sort prior to sending back to the front-end app. I know I can create a view and do it in sql - but I'd like to know if it's possible to do in code.
import {Arg, Ctx, Field, FieldResolver, ID, InputType, ObjectType, Query, Resolver, Root,} from 'type-graphql'
import {Client} from '../entities/Client'
import {ClientNameSort, Company} from '../entities/Company'
import {MyContext} from '../types/MyContext'
#InputType()
export class ClientsOptions {
#Field(() => ID)
companyId!: string
}
#ObjectType()
#Resolver(Client)
export class ClientResolver {
#FieldResolver(() => String)
async sortName(#Root() client: Client, #Ctx() { companyLoader }: MyContext) {
const company:Company = await companyLoader.load(client.companyId)
if (!company) {
throw new Error(`Missing rec for Company Id ${client.companyId}`)
}
if (company.clientNameSort === ClientNameSort.FIRST_NAME) {
return `${client.firstName} ${client.lastName} ${client.id}`
} else {
return `${client.lastName} ${client.firstName} ${client.id}`
}
}
#Query(() => [Client])
async clients(#Arg('options') options: ClientsOptions) {
const clientList = await Client.find({ where: { companyId: options.companyId } })
return clientList;
}
}
I believe you have to do the sorting on the "clients" method. Therefore i am not sure the field resolver is going to help you. You better separate that into a utility function and reuse it in both methods.
To to the sorting on the clients method:
if you use mongodb, maybe you can do an aggregated query that will create this virtual field and order by it
if you use another DB , you will have to look if they have this kind of feature
If you don't want to do sorting through DB, you can get the array of clients and do a sort
clients.sort((a,b) => sortByCompanyName(company.CLIENT_NAME_SORT, a, b))
getFullIdentificator(order, client) {
if (order === ClientNameSort.FIRST_NAME) {
return `${client.firstName} ${client.lastName} ${client.id}`
} else {
return `${client.lastName} ${client.firstName} ${client.id}`
}
}
sortByCompanyName(order, prev, next) {
return getFullIdentificator(order, prev) > getFullIdentificator(order, next) ? 1 : -1
}

Is smart query custom variable name possible?

I'm using Vue alongside with Apollo in order to query a GraphQL endpoint in my project. Everything's fine but I want to start programming generic components to ease and fasten the development.
The thing is, in most views, I use the Smart Query system.
For instance, I use :
apollo: {
group: {
query: GROUP_QUERY,
variables () { return { id: this.groupId } },
skip () { return this.groupId === undefined },
result ({ data }) {
this.form.name = data.group.name
}
}
}
With the GROUP_QUERY that is :
const GROUP_QUERY = gql`
query groupQuery ($id: ID) {
group (id: $id) {
id
name
usersCount
userIds {
id
username
}
}
}
`
So my group variable in my apollo smart query has the same name as the query itself group (id: $id). It is this mechanism that is quite annoying for what I try to achieve. Is there a way to avoid that default mechanism ?
I'd like for instance to be able to give a generic name such as record, and it would be records for queries that potentially return multiple records.
With that, I would be able to make generic components or mixins that operate either on record or records.
Or have I to rename all my queries to record and records which would be annoying later on in case of troubleshooting with error messages ?
Or maybe there's another way to achieve that and I didn't think about it ?
Thanks in advance.
You can, in fact, rename the variable of Apollo smart queries using the update option, as seen here in the Vue Apollo documentation. Your example would look like:
apollo: {
record: {
query: GROUP_QUERY,
variables () { return { id: this.groupId } },
update: (data) => data.group,
skip () { return this.groupId === undefined },
result ({ data }) {
this.form.name = data.group.name
}
}
}
You should notice that the Apollo object will create a record variable in your component, and the update statement shows where to get the group for the record.
By doing so :
const GROUP_QUERY = gql`
query groupQuery ($id: ID) {
record: group (id: $id) {
id
name
usersCount
userIds {
id
username
}
}
}
`
If the GROUP_QUERY is used at several places, the result will be accessible under the record name, because it is defined as an alias over group.
See documentation for Aliases.

Load only the data that's needed from database with Graphql

I'm learning graphql and I think I've spot one flaw in it.
Suppose we have schema like this
type Hero {
name: String
friends: [Person]
}
type Person {
name: String
}
and two queries
{
hero {
name
friends {
name
}
}
}
and this
{
hero {
name
}
}
And a relational database that have two corresponding tables Heros and Persons.
If my understanding is right I can't resolve this queries such that for the first query the resulting sql query would be
select Heros.name, Persons.name
from Heros, Persons
where Hero.name = 'Some' and Persons.heroid = Heros.id
And for the second
select Heros.name, Persons.name from Heros
So that only the fields that are really needed for the query would be loaded from the database.
Am I right about that?
Also if graphql would have ability to return only the data that's needed for the query, not the data that's valid for full schema I think this would be possible, right?
Yes, this is definitely possible and encouraged. However, the gist of it is that GraphQL essentially has no understanding of your storage layer until you explicitly explain how to fetch data. The good news about this is that you can use graphql to optimize queries no matter where the data lives.
If you use javascript, there is a package graphql-fields that can simplify your life in terms of understanding the selection set of a query. It looks something like this.
If you had this query
query GetCityEvents {
getCity(id: "id-for-san-francisco") {
id
name
events {
edges {
node {
id
name
date
sport {
id
name
}
}
}
}
}
}
then a resolver might look like this
import graphqlFields from 'graphql-fields';
function getCityResolver(parent, args, context, info) {
const selectionSet = graphqlFields(info);
/**
selectionSet = {
id: {},
name: {},
events: {
edges: {
node: {
id: {},
name: {},
date: {},
sport: {
id: {},
name: {},
}
}
}
}
}
*/
// .. generate sql from selection set
return db.query(generatedQuery);
}
There are also higher level tools like join monster that might help with this.
Here is a blog post that covers some of these topics in more detail. https://scaphold.io/community/blog/querying-relational-data-with-graphql/
In Scala implementation(Sangria-grahlQL) you can achieve this by following:
Suppose this is the client query:
query BookQuery {
Books(id:123) {
id
title
author {
id
name
}
}
}
And this is your QueryType in Garphql Server.
val BooksDataQuery = ObjectType(
"data_query",
"Gets books data",
fields[Repository, Unit](
Field("Books", ListType(BookType), arguments = bookId :: Nil, resolve = Projector(2, (context, fields) =>{ c.ctx.getBooks(c.arg(bookId), fields).map(res => res)}))
)
)
val BookType = ObjectType( ....)
val AuthorType = ObjectType( ....)
Repository class:
def getBooks(id: String, projectionFields: Vector[ProjectedName]) {
/* Here you have the list of fields that client specified in the query.
in this cse Book's id, title and author - id, name.
The fields are nested, for example author has id and name. In this case author will have sequence of id and name. i.e. above query field will look like:
Vector(ProjectedName(id,Vector()), ProjectedName(title,Vector()),ProjectedName(author,ProjectedName(id,Vector()),ProjectedName(name,Vector())))
Now you can put your own logic to read and parse fields the collection and make it appropriate for query in database. */
}
So basically, you can intercept specified fields by client in your QueryType's field resolver.

How do I run multiple queries in sailsjs controller?

It seems in sailsjs you can only run and pass one set of query data at a time. For example here is the controller for my homepage:
module.exports = {
index: function (req, res) {
Blog.find()
.limit(3)
.sort('createdAt desc')
.where({ isPublished: 1 })
.exec(function(err, posts) {
if (err) return next(err);
res.view({
layout: "homeLayout",
posts:posts
});
});
}
};
How would I query data from some other model and pass it to my view along with the blog data Im already passing?
You can use Promises to do so. It's actually an excellent usecase.
I use Q, which is what Waterline (Sail's ORM) use behind the scene.
You can see below an example of code where I retrieve data from a first model, and then, using the data I retrieved, I query other models to get some more data (in parallel), and in the end, I send the result back to the view.
SomeModel.findOne(criterias).then(function(result) {
Q.all([
SomeOtherModel.getSomething(result),
YetAnotherModel.getSomethingElse(result)
]).spread(function(someOtherResult, yetAnotherResult) {
var data = {
thing: result,
stuff: someOtherResult,
otherthing: yetAnotherResult
};
return res.view(data);
});
}).fail(function(reason) {
return res.view(reason);
});
The getSomething() function should return a promise, standard finder from Sails will work transparently (just don't pass the callback). As per this other question it appears that standard finder do not behave exactly like Q promises, the answer I gave there should help get a more consistant behavior.
More on Q and how it works in the doc !
You could also use async.auto (see below). Here's a link to the complete sails repo example.
var async = require('async'),
_ = require('lodash');
module.exports = {
index: function (req, res) {
async.auto({
// Get the blog posts
posts: function (cb) {
Blog.find()
.where({ isPublished: 1 })
.limit(5)
.sort('createdAt DESC')
.exec(cb);
},
// Get some more stuff
// (this will happen AT THE SAME TIME as `posts` above)
otherThings: function (cb) {
OtherThing.find()
.limit(30)
.exec(cb);
},
// Get comments
// (we'll wait until `posts` is finished first)
comments: ['posts', function (cb, async_data) {
// Get `posts`
// (the second argument to cb() back in `posts`)
// Used map to make sure posts are an array of ids and not just an object.
var posts = async_data.posts.map(function (item){ return item.id});
// Get comments that whose `post_id` is equal to
// the id of one of the posts we found earlier
Comment.find()
.where({ post_id: posts })
.exec(cb);
}]
},
function allDone (err, async_data) {
// If an error is passed as the first argument to cb
// in any of the functions above, then the async block
// will break, and this function will be called.
if (err) return res.serverError(err);
var posts = async_data.posts;
var comments = async_data.comments;
var otherThings = async_data.otherThings;
// Fold the comments into the appropriate post
// An in-memory join
_.map(posts, function (post) {
var theseComments =
_.where(comments, { post_id: post.id });
post.comments = theseComments;
});
// Show a view using our data
res.json({
// layout: 'homeLayout',
posts: posts,
otherThings: otherThings
});
});
}
};
I have figured out a few ways to accomplish this. The first way is to nest your queries, eg.
Blog.find()
.limit(30)
.sort('createdAt desc')
.where({ isPublished: 1 })
.exec(function(err, posts) {
SomeOtherModel.find()
.limit(5)
.sort('createdAt desc')
.where({ isPublished: 1 })
.exec(function(err, otherdata) {
res.view({
posts: posts,
otherdata: otherdata
});
});
});
The second way is to use promises (I wasnt aware of this previously)
User.findOne()
.where({ id: 2 })
.then(function(user){
var comments = Comment.find({userId: user.id}).then(function(comments){
return comments;
});
return [user.id, user.friendsList, comments];
}).spread(function(userId, friendsList, comments){
// Promises are awesome!
}).fail(function(err){
// An error occured
})
The third way (I ended up going with this) is to create a policy (specific to sailsjs but is express middleware)
// saved as /api/policies/recentPosts.js
// also need to add a rule to /config/policies.js
module.exports = function (req, res, ok) {
Blog.find()
.limit(3)
.sort('createdAt desc')
.where({ isPublished: 1 })
.exec(function(err, footerposts) {
res.footerposts = footerposts;
return ok();
});
};
Doing it this way you dont need to pass anything to your view however Im not sure if its good practice to randomly add data to the response object.
So here is how you can make 3 requests and pass all their data into your view:
first install Q
npm install q
Then use code below and substitute my models with yours:
// first import Q
var Q = require('q');
// Let's combine results of 3 queries
Q.all([
// let's find one user with name "Pavel"
User.findOne({name: 'Pavel'}).then(),
// let's find one Lexus car
Cars.findOne({brand: 'Lexus'}).then(),
// Finally let's get the first Apple phone
Phones.findOne({brand: 'Apple'}).then()
])
.spread(function (user, car, phone) {
// Output results as json, but you can do whatever you want here
res.json([user, car, phone]);
}).fail(function (reason) {
// output reason of failure
res.json(reason);
});