I have a question about handling promises in resolve functions for a GraphQL client. Traditionally, resolvers would be implemented on the server, but I am wrapping a REST API on the client.
Background and Motivation
Given resolvers like:
const resolvers = {
Query: {
posts: (obj, args, context) => {
return fetch('/posts').then(res => res.json());
}
},
Post: {
author: (obj, args, _, context) => {
return fetch(`/users/${obj.userId}`)
.then(res => res.json());
.then(data => cache.users[data.id] = data)
}
}
};
If I run the query:
posts {
author {
firstName
}
}
and the Query.posts() /posts API returns four post objects:
[
{
"id": 1,
"body": "It's a nice prototyping tool",
"user_id": 1
},
{
"id": 2,
"body": "I wonder if he used logo?",
"user_id": 2
},
{
"id": 3,
"body": "Is it even worth arguing?",
"user_id": 1
},
{
"id": 4,
"body": "Is there a form above all forms? I think so.",
"user_id": 1
}
]
the Post.author() resolver will get called four times to resolve the author field.
grapqhl-js has a very nice feature where each of the promises returned from the Post.author() resolver will execute in parallel.
I've further been able to eliminate re-fetching author's with the same userId using facebook's dataloader library. BUT, I'd like to use a custom cache instead of dataloader.
The Question
Is there a way to prevent the Post.author() resolver from executing in parallel? Inside the Post.author() resolver, I would like to fetch authors one at a time, checking my cache in between to prevent duplicate http requests.
But, right now the promises returned from Post.author() are queued and executed at once, so I cannot check the cache before each request.
Thank you for any tips!
I definitely recommend looking at DataLoader as it's designed to solve exactly this problem. If you don't use it directly, at least you can read its implementation (which is not that many lines) and borrow the techniques atop your custom cache.
GraphQL and the graphql.js libraries themselves are not concerned with loading data - they leave that up to you via resolver functions. Graphql.js is just calling these resolver functions as eagerly as it can to provide for the fastest overall execution of your query. You can absolutely decide to return Promises which resolve sequentially (which I wouldn't recommend), or—as DataLoader implements—deduplicate with memoization (which is what you want for solving this).
For example:
const resolvers = {
Post: {
author: (obj, args, _, context) => {
return fetchAuthor(obj.userId)
}
}
};
// Very simple memoization
var authorPromises = {};
function fetchAuthor(id) {
var author = authorPromises[id];
if (!author) {
author = fetch(`/users/${id}`)
.then(res => res.json());
.then(data => cache.users[data.id] = data);
authorPromises[id] = author;
}
return author;
}
Just for some people who use dataSource for REST api stuff along with dataLoader(in this case, it doesn't really help as it's a single request). Here is a simple caching solution/example.
export class RetrievePostAPI extends RESTDataSource {
constructor() {
super()
this.baseURL = 'http://localhost:3000/'
}
postLoader = new DataLoader(async ids => {
return await Promise.all(
ids.map(async id => {
if (cache.keys().includes(id)) {
return cache.get(id)
} else {
const postPromise = new Promise((resolve, reject) => {
resolve(this.get(`posts/${id}`))
reject('Post Promise Error!')
})
cache.put(id, postPromise, 1000 * 60)
return postPromise
}
})
)
})
async getPost(id) {
return this.postLoader.load(id)
}
}
Note: here I use memory-cache for caching mechanism.
Hope this helps.
Related
I am really struggling with this concept. I hope someone can help me understand it better.
The documentations uses a simple example and it's not 100% clear to me how it works.
I have tried using keyArgs, but they didn't work, so I adopted to use the args parameter in the read and merge functions. First, let me explain my scenario.
I have a couple of search endpoints that use the same parameters:
{
search:
{
searchTerm: "*",
includePartialMatch: true,
page: 1,
itemsToShow: 2,
filters: {},
facets: [],
orderBy: {}
}
}
So I have setup my type policies like this:
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
searchCategories: typePolicy,
searchBrands: typePolicy,
searchPages: typePolicy,
searchProducts: typePolicy,
},
},
},
});
And I was using a generic typePolicy for them all.
At first, I tried this:
const typePolicy = {
keyArgs: [
"search",
[
"identifier",
"searchTerm",
"includePartialMatches",
"filters",
"orderBy",
"facets",
],
],
// Concatenate the incoming list items with
// the existing list items.
merge(existing: any, incoming: any) {
console.log("existing", existing);
console.log("incoming", incoming);
if (!existing?.items) console.log("--------------");
if (!existing?.items) return { ...incoming }; // First request
const items = existing.items.concat(incoming.items);
const item = { ...existing, ...incoming };
item.items = items;
console.log("merged", item);
console.log("--------------");
return item;
},
};
But this does not do what I want.
What I would like, is for apollo to work as it does normally, but when the "page" changes for any field, it appends it instead of caching a new request.
Does anyone know what I am doing wrong or can provide me with a better example that what is on the documentation?
After having implemented dataloader in the respective resolvers to solve the N+1 problem, I also need to be able to solve the N+N problem.
I need a decently efficient data loading mechanism to get a relation like this:
{
persons (active: true) {
id,
given_name,
projects (active: true) {
id,
title,
}
}
}
I've created a naive implementation for this, returning
{
persons: [
{
id: 1,
given_name: 'Mike'
projects: [
{
id: 1,
title: 'API'
},
{
id: 2,
title: 'Frontend'
}
]
}
{
id: 2,
given_name: 'Eddie'
projects: [
{
id: 2,
title: 'Frontend'
},
{
id: 3,
title: 'Testing'
}
]
}
]
}
In SQL the underlying structure would be represented by a many many to many relationship.
Is there a similiar tool like dataloader for solving this or can this maybe even be solved with dataloader itself?
The expectation with GraphQL is that the trip to the database is generally the fastest thing you can do, so you just add a resolver to Person.projects that makes a call to the database. You can still use dataLoaders for that.
const resolvers = {
Query: {
persons(parent, args, context) {
// 1st call to database
return someUsersService.list()
},
},
Person: {
projects(parent, args, context) {
// this should be a dataLoader behind the scenes.
// Makes second call to database
return projectsService.loadByUserId(parent.id)
}
}
}
Just remember that now your dataLoader is expecting to return an Array of objects in each slot instead of a single object.
Let's say I have a field called user with a data that looks something like this
{
"id": "abc123",
"name": "John Smith"
}
I want to make a route where I can find where user.id equals, say, abc123 and should return the blogs that has a user with the id above
I've tried doing
async findByUser(ctx) {
let blogs = await strapi.services.blogs.find({
user: {id:ctx.params.id},
return blogs;
},
but that doesn't seem to work as it returns an empty array and isn't searching specifically in the id property. How do I do this using strapi?
edit: User is not an relation, it is an individual JSON field.
Okay, for querying a JSON object property, you will need to write a custom query. Look at the example below.
Implementation for PostGreSQL
async findByUser(ctx) {
const response = await strapi
.query('blogs')
.model.query((qb) => {
qb.where('user', '#>', `{"id": "${ctx.params.id}" }`);
// qb.where('user', '#>', `{"name": "${ctx.params.name}" }`);
})
.fetch();
return response.toJSON();
},
Implementation for SQLite
async findByUser(ctx) {
const response = await strapi
.query('blogs')
.model.query((qb) => {
qb.where('user', 'LIKE', `%"id":"${ctx.params.id}"%`);
})
.fetch();
return response.toJSON();
},
P.S: Just use fetch instead of fetchAll for consistency.
Hi there thanks to Salvino's help I think i am able to find a solution
async findByUser(ctx) {
const response = await strapi
.query('blogs')
.model.query((qb) => {
qb.where('user', 'LIKE', `%"id":"${ctx.params.id}"%`);
})
.fetchAll();
return response.toJSON();
},
Receiving data with AppSync directly from DynamoDB seems working for my case, but when I try to put a lambda function in between, I receive errors that says "Can't resolve value (/issueNewMasterCard/masterCards) : type mismatch error, expected type LIST"
Looking to the AppSync cloudwatch response mapping output, I get this:
"context": {
"arguments": {
"userId": "18e946df-d3de-49a8-98b3-8b6d74dfd652"
},
"result": {
"Item": {
"masterCards": {
"L": [
{
"M": {
"cardId": {
"S": "95d67f80-b486-11e8-ba85-c3623f6847af"
},
"cardImage": {
"S": "https://s3.eu-central-1.amazonaws.com/logo.png"
},
"cardWallet": {
"S": "0xFDB17d12057b6Fe8c8c434653456435634565"
},...............
here is how I configured my response mapping template:
$utils.toJson($context.result.Item)
I'm doing this mutation:
mutation IssueNewMasterCard {
issueNewMasterCard(userId:"18e946df-d3de-49a8-98b3-8b6d74dfd652"){
masterCards {
cardId
}
}
}
and this is my schema :
type User {
userId: ID!
masterCards: [MasterCard]
}
type MasterCard {
cardId: String
}
type Mutation {
issueNewMasterCard(userId: ID!): User
}
The Lambda function:
exports.handler = (event, context, callback) => {
const userId = event.arguments.userId;
const userParam = {
Key: {
"userId":{S:userId}
},
TableName:"FidelityCardsUsers"
}
dynamoDB.getItem(userParam, function(err, data) {
if (err) {
console.log('error from DynamDB: ',err)
callback(err);
} else {
console.log('mastercards: ',JSON.stringify(data));
callback(null,data)
}
})
I think the problem is that the getItem you use when you use the DynamoDB datasource is not the same as the the DynamoDB.getItem function in the aws-sdk.
Specifically it seems like the datasource version returns an already marshalled response (that is, instead of something: { L: [ list of things ] } it just returns something: [ list of things]).
This is important, because it means that $utils.toJson($context.result.Item) in your current setup is returning { masterCards: { L: [ ... which is why you are seeing the type error- masterCards in this case is an object with a key L, rather than an array/list.
To solve this in the resolver, you can use the $util.dynamodb.toDynamoDBJson(Object) macro (https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference.html#dynamodb-helpers-in-util-dynamodb). i.e. your resolver should be:
$util.dynamodb.toDynamoDBJson($context.result.Item)
Alternatively you might want to look at the AWS.DynamoDB.DocumentClient class (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html). This includes versions of getItem, etc. that automatically marshal and unmarshall the proprietary DynamoDB typing back into native JSON. (Frankly I find this much nicer to work with and use it all the time).
In that case you can keep your old resolver, because you'll be returning an object where masterCards is just a JSON array.
I'm attempting to use graphql to tie together a number of rest endpoints, and I'm stuck on how to filter, sort and page the resulting data. Specifically, I need to filter and/or sort by nested values.
I cannot do the filtering on the rest endpoints in all cases because they are separate microservices with separate databases. (i.e. I could filter on title in the rest endpoint for articles, but not on author.name). Likewise with sorting. And without filtering and sorting, pagination cannot be done on the rest endpoints either.
To illustrate the problem, and as an attempt at a solution, I've come up with the following using formatResponse in apollo-server, but am wondering if there is a better way.
I've boiled down the solution to the most minimal set of files that i could think of:
data.js represents what would be returned by 2 fictional rest endpoints:
export const Authors = [{ id: 1, name: 'Sam' }, { id: 2, name: 'Pat' }];
export const Articles = [
{ id: 1, title: 'Aardvarks', author: 1 },
{ id: 2, title: 'Emus', author: 2 },
{ id: 3, title: 'Tapir', author: 1 },
]
the schema is defined as:
import _ from 'lodash';
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLList,
GraphQLString,
GraphQLInt,
} from 'graphql';
import {
Articles,
Authors,
} from './data';
const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: {
id: {
type: GraphQLInt,
},
name: {
type: GraphQLString,
}
}
});
const ArticleType = new GraphQLObjectType({
name: 'Article',
fields: {
id: {
type: GraphQLInt,
},
title: {
type: GraphQLString,
},
author: {
type: AuthorType,
resolve(article) {
return _.find(Authors, { id: article.author })
},
}
}
});
const RootType = new GraphQLObjectType({
name: 'Root',
fields: {
articles: {
type: new GraphQLList(ArticleType),
resolve() {
return Articles;
},
}
}
});
export default new GraphQLSchema({
query: RootType,
});
And the main index.js is:
import express from 'express';
import { apolloExpress, graphiqlExpress } from 'apollo-server';
var bodyParser = require('body-parser');
import _ from 'lodash';
import rql from 'rql/query';
import rqlJS from 'rql/js-array';
import schema from './schema';
const PORT = 8888;
var app = express();
function formatResponse(response, { variables }) {
let data = response.data.articles;
// Filter
if ({}.hasOwnProperty.call(variables, 'q')) {
// As an example, use a resource query lib like https://github.com/persvr/rql to do easy filtering
// in production this would have to be tightened up alot
data = rqlJS.query(rql.Query(variables.q), {}, data);
}
// Sort
if ({}.hasOwnProperty.call(variables, 'sort')) {
const sortKey = _.trimStart(variables.sort, '-');
data = _.sortBy(data, (element) => _.at(element, sortKey));
if (variables.sort.charAt(0) === '-') _.reverse(data);
}
// Pagination
if ({}.hasOwnProperty.call(variables, 'offset') && variables.offset > 0) {
data = _.slice(data, variables.offset);
}
if ({}.hasOwnProperty.call(variables, 'limit') && variables.limit > 0) {
data = _.slice(data, 0, variables.limit);
}
return _.assign({}, response, { data: { articles: data }});
}
app.use('/graphql', bodyParser.json(), apolloExpress((req) => {
return {
schema,
formatResponse,
};
}));
app.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
}));
app.listen(
PORT,
() => console.log(`GraphQL Server running at http://localhost:${PORT}`)
);
For ease of reference, these files are available at this gist.
With this setup, I can send this query:
{
articles {
id
title
author {
id
name
}
}
}
Along with these variables (It seems like this is not the intended use for the variables, but it was the only way I could get the post processing parameters into the formatResponse function.):
{ "q": "author/name=Sam", "sort": "-id", "offset": 1, "limit": 1 }
and get this response, filtered to where Sam is the author, sorted by id descending, and getting getting the second page where the page size is 1.
{
"data": {
"articles": [
{
"id": 1,
"title": "Aardvarks",
"author": {
"id": 1,
"name": "Sam"
}
}
]
}
}
Or these variables:
{ "sort": "-author.name", "offset": 1 }
For this response, sorted by author name descending and getting all articles except the first.
{
"data": {
"articles": [
{
"id": 1,
"title": "Aardvarks",
"author": {
"id": 1,
"name": "Sam"
}
},
{
"id": 2,
"title": "Emus",
"author": {
"id": 2,
"name": "Pat"
}
}
]
}
}
So, as you can see, I am using the formatResponse function for post processing to do the filtering/paging/sorting. .
So, my questions are:
Is this a valid use case?
Is there a more canonical way to do filtering on deeply nested properties, along with sorting and paging?
Is this a valid use case? Is there a more canonical way to do filtering on deeply nested properties, along with sorting and paging?
Major part of original questing lies on segregating collections on different databases on separate microservices. In fact, it's nessasary to perform collection joining and subsequent filtering on some key, but it's directly impossible since there is no field in original collection to filter, sort or paginate.
Strightforward solution is perform full or filtered queries to original collections, and then perform joining and filtering result dataset on application server, e.g. by lodash, such at your solution. In is possible for small collections, but in general case causes large data transfer and unefficent sorting since there is no index structure - real RB-tree or SkipList, so with quadratic complexity it's not very good.
Dependent on resource volume on application server, special cache and index tables can be build there. If collection structure is fixed, some relations between collection entries and their fields can be reflected in special search table and update respectively on demain. It's like find & search index creation, but not it database, but on application server. Of cource, it will consume resources, but will be more fast than direct lodash-like sorting.
Also task can be solved from another side, if there is access to structure of original databases. Key is denormalization. In counter for classical relation approach, collections can have dublicate information for avioding further join operation. E.g., Articles collection can have some information from Authors collection, which is nessasary to perform filtering, sorting and pagination in further operations.