Urql cache invalidate is not removing items from the cache - graphql

My application has a list of organizations and I have buttons in my UI to delete them individually.
I would like the list to update and remove the deleted organization but that is not working for me.
I have set up a cache exchange like this, where I have (redundantly) tried two cache invalidation methods from the Urql docs:
const cache = cacheExchange({
updates: {
Mutation: {
delOrg(_result, args, cache, _info) {
// Invalidate cache based on type
cache.invalidate({ __typename: 'Org', id: args.id as number });
// Invalidate all fields
const key = 'Query';
cache
.inspectFields(key)
.filter((field) => field.fieldName === 'allOrgs')
.forEach((field) => {
cache.invalidate(key, field.fieldKey);
});
}
}
}
});
The GraphQL query that returns the list of organizations looks like:
query AllOrgs {
allOrgs {
id
name
logo {
id
url
}
}
}
And the mutation to delete an organization looks like: (it returns a boolean)
mutation DelOrg($id: ID!) {
delOrg(id: $id)
}
cache.invalidate does not appear to do anything. I have checked the cache using the debugging plugin as well as console.log. I can see the records in the cache and they don't get removed.
I am using
"#urql/exchange-graphcache": "^4.4.1",
"#urql/svelte": "^1.3.3",

I found a way around my issue. I now use cache.updateQuery to update data rather than a single line, I now have -
updates: {
Mutation: {
delOrg(_result, args, cache, _info) {
if (_result.delOrg) {
cache.updateQuery(
{
query: QUERY_ALL_ORGS
},
(data) => {
data = {
...data,
allOrgs: data.allOrgs.filter((n) => n.id !== args.id)
};
return data;
}
);
}
}
}
}

If it's just about cache invalidation, you can use the following invalidateCache function:
const cacheConfig: GraphCacheConfig = {
schema,
updates: {
Mutation: {
// create
createContact: (_parent, _args, cache, _info) =>
invalidateCache(cache, 'allContacts'),
// delete
deleteContactById: (_parent, args, cache, _info) =>
invalidateCache(cache, 'Contact', args),
},
},
}
const invalidateCache = (
cache: Cache,
name: string,
args?: { input: { id: any } }
) =>
args
? cache.invalidate({ __typename: name, id: args.input.id })
: cache
.inspectFields('Query')
.filter((field) => field.fieldName === name)
.forEach((field) => {
cache.invalidate('Query', field.fieldKey)
})
For creation mutations - with no arguments - the function inspects the root Query and invalidates all field keys having matching field names.
For deletion mutations the function invalidates the key that must have been given as a mutation argument.

Related

How to organize GraphQL resolver for additional fields

Let's say I have a simple GraphQL type for a user:
type User {
id: ID!
name: String!
}
Query {
user(id:ID!)
}
and a resolver
user = (_, {id}, {api})=> api.getUser(id)
Now I have add a new field to the User called friends and added a new resolver for the User.friends field.
friends = ({id}, _, {api})=> api.getFriends(id)
So now I wonder when we made a query like this, how can I prevent the call to api.getUser but only call api.getFriends.
query {
user(id){
friends {
name
}
}
}
My understanding is that having a resolver defined for the user field in the Query type, it will always call this resolver first and after that all resolvers for fields in the User type.
This is a common problem and there is for example this solution out there: https://github.com/gajus/graphql-lazyloader
Check out the README of the project for a structured description of your problem.
Alternatively, you can implement your own class that contains a cached value making use of how GraphQL.js implements default resolvers:
class User {
constructor(id) {
this.id = id;
}
getInstance({ api }) {
if (!this.instance) {
this.instance = api.getUser(this.id);
}
return this.instance;
}
// notice how id is already a property of this class
name(args, ctx) {
return this.getInstance(ctx).then(instance => instance.name);
}
// do the same for other fields, user will only be fetched once.
friends(args, { api }) {
return api.getFriends(this.id);
}
}
const resolvers = {
Query: {
user: (args) => new User(args.id),
}
}
If you use dataloader you can even do this with even less code thanks to caching in dataloader:
// You probably have this function already somewhere in your apollo server creation
function createContext({ api }) {
return {
api,
loaders: {
user: new Dataloader((ids) => ids.map(id => api.getUser(id))),
},
}
}
const resolvers = {
Query: {
user: (parent, args) => ({ id: args.id }),
},
User: {
name: ({ id }, args, { loaders }) =>
loaders.user.load(id).then(user => user.name),
otherProp: ({ id }, args, { loaders }) =>
loaders.user.load(id).then(user => user.otherProp),
friends: ({ id }, args, { api })=> api.getFriends(id),
}
}
Dataloader will, even when called twice, only reach to the API once. An added benefit is, that it will cache the value. Ideally, you even provide a batch load function in the API to make the loader even more efficient.
Be aware, that user.fields.name now makes calls for every friend to the API. To avoid that, you could check if the property exists:
name: (parent, args, { loaders }) =>
parent.name ?? loaders.user.load(parent.id).then(user => user.name),

How do I query my API for a single entity by its "slug" with GraphQL?

I am creating a Next.js blog that uses an API created with KeystoneJS. I am extremely confused by how I can get an individual post on a dynamic route from the post's slug.
The Query
This is how I thought the query should be:
query Post($slug: String) {
Post(where: { slug: $slug }) {
id
}
}
And this was queried like so in a file called post.service.js:
export async function getBySlug(slug) {
return apolloClient
.query({
query: gql`
query Post($slug: String) {
Post(where: { slug: $slug }) {
id
}
}
`,
})
.then((result) => {
return result.data.Post;
});
}
Unsurprisingly, that causes an ApolloError because how would the query know what slug to query the API for when accessing posts/[slug].js?
It's also worth noting that KeystoneJS say on their guides that:
The single entity query accepts a where parameter which must provide an id.
How would I pass the post's ID to the query depending on what slug was accessed at [slug].js and does this mean I can't query by the slug at all?
On [slug].js I am using getStaticPaths() and getStaticProps() like this:
export async function getStaticPaths() {
const posts = await getAll();
const paths = posts.map((post) => ({
params: { slug: post.slug },
}));
return { paths, fallback: false };
}
export async function getStaticProps({ params }) {
const term = await getBySlug(params.slug);
return { props: { post } };
}
How can I do this?
If you're using a where clause rather than matching on id, you have to query allPosts rather than Post.
A tested example, matching a user by their email address:
query($email: String!) {
allUsers(where : {email: $email}){
id
}
}
Variables:
{
"email": "user#email.com"
}
So I think you want:
query($slug: String!) {
allPosts(where: {slug: $slug}) {
id
}
}

How can I use Apollo's cacheRedirect with a nested query

I've got a query that looks like this:
export const GET_PROJECT = gql`
query GetProject($id: String!) {
homework {
getProject(id: $id) {
...ProjectFields
}
}
}
${ProjectFieldsFragment}
`;
My InMemoryCache looks like this:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
getProject: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
The above cache redirect is never hit. However, if I modify it to look like:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
homework: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
It does get hit, however I don't have any of the arguments that are passed in the nested getProject query. What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for, like:
export const SESSION = gql`
query Session {
session {
user {
id
fullName
email
}
organizations {
name
id
}
}
}
`;
So what is going on? I've resorted to just using readFragment in the places where I want the cache to redirect, but I'd like for that logic to become centralized.
It's hard to say for sure with these kinds of issues, but I'm betting that, since you say
What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for
the issue might be with your dataIdFromObject function.
This function is ultimately what decides if data is read from the cache or not. You should only override this if you have a very specific reason to. For example:
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
// ...
export default new ApolloClient({
link,
cache: new InMemoryCache({
dataIdFromObject(object) {
switch (object.__typename) {
case 'ModifierScale':
case 'ModifierGroup':
return [
object.__typename,
object.id,
...object.defaults
.map((defaultModifier) => defaultModifier.id)
.join(''),
].join('');
default:
return defaultDataIdFromObject(object); // fall back to default handling
}
},
}),
});
The point of this setting is to allow you to customize the key that gets put into the cache when you are loading the data.
If this doesn't solve your issue, I would definitely go into the Apollo tab in the chrome dev tools (you need the Apollo dev tools chrome extension to do this) and look at the cache section. It should show you the data in the cache and the key that the data is stored in.

Is it possible to add another field in the final response of GraphQL query?

I've been trying to research on how to add another root property of a GraphQL response but found nothing after 1 hour.
Normally, a GraphQL query looks like this:
{
myQuery() {
name
}
}
It responds with:
{
"data": {
"myQuery": []
}
}
I'm curious if I can add another root property in this response say "meta"
{
"data": {
"myQuery": []
},
"meta": {
"page": 1,
"count": 10,
"totalItems": 90
}
}
Is this possible, if not what's the best approach in tackling this with respect to GraphQL?
Thanks!
The apollo-server middleware can be configured with a number of configuration options, including a formatResponse function that allows you to modify the outgoing GraphQL response
const formatResponse = (response) => {
return {
meta
...response
}
}
app.use('/graphql', bodyParser.json(), graphqlExpress({
schema,
formatResponse,
}));
You could pass the req object down to your context, mutate it within your resolver(s) and then use the result inside formatResponse. Something like...
app.use('/graphql', bodyParser.json(), (req, res, next) => graphqlExpress({
schema,
formatResponse: (gqlResponse) => ({
...gqlResponse
meta: req.metadata
}),
})(req, res, next));
Typically, though, you would want to include the metadata as part of your actual schema and have it included with the data. That will also allow you to potentially request multiple queries and get the metadata for all of them.
There's any number of ways to do that, depending on how your data is structured, but here's an example:
type Query {
getFoos: QueryResponse
getBars: QueryResponse
}
type QueryResponse {
results: [Result]
meta: MetaData
}
union Result = Bar | Foo
You can add anything in the response as well... Please follow below code.
app.use('/graphql', bodyParser.json(), graphqlExpress(req => {
return {
schema: tpSchemaNew,
context: {
dbModel
},
formatError: err => {
if (err.originalError && err.originalError.error_message) {
err.message = err.originalError.error_message;
}
return err;
},
formatResponse : res => {
res['meta'] = 'Hey';
return res;
}
}
}))
Apollo Server-specific:
Just adding to the previous answers that formatResponse() has another useful argument, requestContext.
If you are interested in extracting values from that (for example, the context passed to the resolver), you can do the following. BEWARE HOWEVER, the context will likely contain sensitive data that is supposed to be private. You may be leaking authentication data and secrets if not careful.
const server = new ApolloServer({
schema,
formatResponse: (response, requestContext) => {
//return response
const userId = requestContext.context.user.id
response = Object.assign(response, {
extensions: {
meta: {
userId: userId
}
}
}
return response
},
})
The above will return something like this in the gql query response (note the extensions object):
{
data: {
user: {
firstName: 'Hello',
lastName: 'World'
}
},
extensions: { // <= in Typescript, there is no `meta` in GraphQLResponse, but you can use extensions
meta: {
userId: 1234 //<= data from the context
}
}
}
The full list of properties available in requestContext:
at node_modules/apollo-server-types/src/index.ts>GraphQLRequestContext
export interface GraphQLRequestContext<TContext = Record<string, any>> {
readonly request: GraphQLRequest;
readonly response?: GraphQLResponse;
readonly context: TContext;
readonly cache: KeyValueCache;
// This will be replaced with the `operationID`.
readonly queryHash?: string;
readonly document?: DocumentNode;
readonly source?: string;
// `operationName` is set based on the operation AST, so it is defined even if
// no `request.operationName` was passed in. It will be set to `null` for an
// anonymous operation, or if `requestName.operationName` was passed in but
// doesn't resolve to an operation in the document.
readonly operationName?: string | null;
readonly operation?: OperationDefinitionNode;
/**
* Unformatted errors which have occurred during the request. Note that these
* are present earlier in the request pipeline and differ from **formatted**
* errors which are the result of running the user-configurable `formatError`
* transformation function over specific errors.
*/
readonly errors?: ReadonlyArray<GraphQLError>;
readonly metrics?: GraphQLRequestMetrics;
debug?: boolean;
}

Elegant and efficient way to resolve related data in GraphQL

What can be the best way to resolve the data in GraphQL
Here i have a SeekerType and JobType, JobsType is nested in SeekerType
A Seeker can apply to many Jobs. When Querying for a seeker, One can just query for seeker's data or as well as he can query for nested JobType and can get the jobstype data too.
But the Question is that If One doesn't Query for nested JobType
he won't get the Jobs data but mine Seeker resolver in viewerType would be fetching that data too.
So, while providing data to the seeker query how can i handle that, Either he can only want seeker details or may want the jobs details too.
Shall I use resolver of each nestedType and get the parent object, and fetch the relevant data using fields from parent Object???
The code below is just for illustration and clarification, the question is about the best way to resolve data
ViewerType.js
const Viewer = new GraphQLObjectType({
name: 'Viewer',
fields: () => ({
Seeker: {
type: SeekerConnection,
args: _.assign({
seekerId: { type: GraphQLID },
status: { type: GraphQLString },
shortlisted: { type: GraphQLInt },
}, connectionArgs),
resolve: (obj, args, auth, rootValue) => {
const filterArgs = getFilters(args) || {};
return connectionFromPromisedArray(getSeekers(filterArgs), args)
.then((data) => {
// getSeekers() provides all the data required for SeekerType fields and it's
JobsType fields
data.args = filterArgs;
return data;
}).catch(err => new Error(err));
},
},
}),
});
SeekerType.js
const SeekerType = new GraphQLObjectType({
name: 'SeekerType',
fields: () => ({
id: globalIdField('SeekerType', obj => obj._id),
userId: {
type: GraphQLID,
resolve: obj => obj._id,
},
email: { type: GraphQLString },
password: { type: GraphQLString },
firstName: { type: GraphQLString },
lastName: { type: GraphQLString },
imageLink: { type: GraphQLString },
education: { type: GraphQLString },
address: { type: GraphQLString },
jobs: {
type: new GraphQLList(JobType),
},
}),
interfaces: [nodeInterface],
});
getSeekers() provide complete data as graphql fields format with nested
jobs field data too
const getSeekers = filterArgs => new Promise((resolve, reject) => {
if (Object.keys(filterArgs).length === 0) {
Seeker.find(filterArgs, { password: 0 }, (err, d) => {
if (err) return reject(err);
return resolve(d);
});
} else {
async.parallel([
(callback) => {
filterArgs._id = filterArgs.seekerId;
delete filterArgs.seekerId;
Seeker.find(filterArgs).lean()
.exec((err, d) => {
if (err) return callback(err);
if (err === null && d === null) return callback(null);
callback(null, d);
});
},
(callback) => {
filterArgs.seekerId = filterArgs._id;
delete filterArgs._id;
Applicant.find(filterArgs).populate('jobId').lean()
.exec((err, resp) => {
if (err) return callback(err);
callback(null, resp);
});
},
], (err, data) => {
const cleanedData = {
userData: data[0],
userJobMap: data[1],
};
const result = _.reduce(cleanedData.userData, (p, c) => {
if (c.isSeeker) {
const job = _.filter(cleanedData.userJobMap,
v => _.isEqual(v.seekerId, c._id));
const arr = [];
_.forEach(job, (i) => {
arr.push(i.jobId);
});
const t = _.assign({}, c, { jobs: arr });
p.push(t);
return p;
}
return reject('Not a Seekr');
}, []);
if (err) reject(err);
resolve(result);
// result have both SeekerType data and nested type
JobType data too.
});
}
});
I gather this to be a question about how to prevent overfetching related data...I.e. How not to necessarily request jobs data when querying the seeker.
This might have several motivations for optimization and security.
Considerations:
If the consumer (e.g. Web app) is under your control, you can simply avoid requesting the jobs field when querying seeker. As you may know, this is one of the stated goals of graphql to only return what is needed over the wire to the consumer, to minimize network traffic and do things in one trip. On the backend I would imagine the graphql engine is smart enough not to overfetch jobs data either, if it's not requested.
If your concern is more of security or unintentional overfetching by consumer apps out of your control, for example, then you can solve that by creating seperate queries and limiting access to them even. E.g. One query for seeker and another for seekerWithJobsData.
Another technique to consider is graphql directives which offer an include switch that can be used to conditionally serve specific fields. One advantage of using this technique in your scenario might be to allow a convenient way to conditionally display multiple fields in multiple queries depending on the value of a single boolean e.g. JobSearchFlag=false. Read here for an overview of directives: http://graphql.org/learn/queries/
I am not sure I completely understand the question but it seems to me you're loading both seeker and job types info on one level. You should load both of them on demand.
On the seeker level, you only get the seeker information, and you can get the IDs of any records related to that seeker. For example, job types ids (if a seeker has many job types)
On the job type level, when used as a nested level for one seeker, you can use those ids to fetch the actual records. This would make the fetching of job types record on-demand when the query asks for it.
The ID to record fetching can be cached and batched with a library like dataloader

Resources