How to organize GraphQL resolver for additional fields - graphql

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),

Related

Urql cache invalidate is not removing items from the cache

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.

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
}
}

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;
}

graphqljs resolver with multiple arguments

I am trying to figure out the best way to write a resolver that filters on multiple arguments. I have the following graphql type
const userQuery = new GraphQLObjectType({
name: 'Query',
fields: {
Users: {
type: new GraphQLList(User),
args: {
userId: { type: GraphQLString }
},
resolve: function (_, { UserId}) {
return new Promise((resolve, reject) => {
//Code to query the data store for the user with the given UserId
})
}
}
}
});
The User type has the following fields
Name
UserId
Type
Gender
Now if I want to introduce the ability to filter the user based on the name, then what is the best way to do it. The only way I can think of is to modify the resolver to include the additional args and then based on what is passed in send it to the database. For example
const userQuery = new GraphQLObjectType({
name: 'Query',
fields: {
Users: {
type: new GraphQLList(User),
args: {
userId: { type: GraphQLString }
},
resolve: function (_, { UserId, name}) {
return new Promise((resolve, reject) => {
//Check which argument is passed in and then run the query against the datastore
})
}
}
}
});
Isn't there a better way to do this? If I want the user to be able to filter on another attribute then it gets more complicated, and the resolve function is going to get huge and complicated.

Relay commitUpdate callback with follow-up mutation and missing fragment

I have two GraphQL/Relay mutations that work fine separately. The first one creates an item. The second one runs a procedure for connecting two items.
GraphQL
createOrganization(
input: CreateOrganizationInput!
): CreateOrganizationPayload
createOrganizationMember(
input: CreateOrganizationMemberInput!
): CreateOrganizationMemberPayload
input CreateOrganizationInput {
clientMutationId: String
organization: OrganizationInput!
}
input CreateOrganizationMemberInput {
clientMutationId: String
organizationMember: OrganizationMemberInput!
}
# Represents a user’s membership in an organization.
input OrganizationMemberInput {
# The organization which the user is a part of.
organizationId: Uuid!
# The user who is a member of the given organization.
memberId: Uuid!
}
type CreateOrganizationPayload {
clientMutationId: String
# The `Organization` that was created by this mutation.
organization: Organization
# An edge for our `Organization`. May be used by Relay 1.
organizationEdge(
orderBy: OrganizationsOrderBy = PRIMARY_KEY_ASC
): OrganizationsEdge
# Our root query field type. Allows us to run any query from our mutation payload.
query: Query
}
I would like to be able to run the createOrganization mutation and then connect the user to the organization with the createOrganizationMember mutation. The second mutation takes two arguments, one of which is the newly created edge.
I tried passing the edge into the mutation, but it expects the mutation to be able to getFragment. How can I get the fragment for the payload edge so it can be passed into a mutation?
React-Relay
Relay.Store.commitUpdate(
new CreateOrganizationMutation({
organizationData: data,
user,
query,
}), {
onSuccess: response => {
Relay.Store.commitUpdate(
new CreateOrganizationMemberMutation({
organization: response.createOrganization.organizationEdge.node,
user,
})
);
},
}
);
fragments: {
user: () => Relay.QL`
fragment on User {
${CreateOrganizationMutation.getFragment('user')},
${CreateOrganizationMemberMutation.getFragment('user')},
}
`,
I solved this problem without changing any GraphQL:
I created a new Relay container, route, and queries object. It is configured as a
child route for the container where the first of two mutation occurs. The id for
the new edge is passed as a parameter via the route pathname. A router state
variable is also passed.
Routes
import {Route} from 'react-router';
function prepareProfileParams (params, {location}) {
return {
...params,
userId: localStorage.getItem('user_uuid'),
};
}
// ProfileContainer has the component CreateOrganizationForm, which calls
// the createOrganization mutation
<Route
path={'profile'}
component={ProfileContainer}
queries={ProfileQueries}
prepareParams={prepareProfileParams}
onEnter={loginBouncer}
renderLoading={renderLoading}
>
<Route path={'join-organization'}>
<Route
path={':organizationId'}
component={JoinOrganizationContainer}
queries={JoinOrganizationQueries}
renderLoading={renderLoading}
/>
</Route>
</Route>
CreateOrganizationForm.js
Relay.Store.commitUpdate(
new CreateOrganizationMutation({
organizationData: data,
user,
query,
}), {
onSuccess: response => {
const organizationId = response.createOrganization.organizationEdge.node.rowId;
router.push({
pathname: `/profile/join-organization/${organizationId}`,
state: {
isAdmin: true,
},
});
},
}
);
The new Relay container JoinOrganizationContainer will hook into a lifecycle
method to call the second mutation that we needed. The second mutation has an
onSuccess callback which does router.push to the page for the new object we
created with the first mutation.
JoinOrganizationContainer.js
import React from 'react';
import Relay from 'react-relay';
import CreateOrganizationMemberMutation from './mutations/CreateOrganizationMemberMutation';
class JoinOrganizationContainer extends React.Component {
static propTypes = {
user: React.PropTypes.object,
organization: React.PropTypes.object,
};
static contextTypes = {
router: React.PropTypes.object,
location: React.PropTypes.object,
};
componentWillMount () {
const {user, organization} = this.props;
const {router, location} = this.context;
Relay.Store.commitUpdate(
new CreateOrganizationMemberMutation({
user,
organization,
isAdmin: location.state.isAdmin,
}), {
onSuccess: response => {
router.replace(`/organization/${organization.id}`);
},
}
);
}
render () {
console.log('Joining organization...');
return null;
}
}
export default Relay.createContainer(JoinOrganizationContainer, {
initialVariables: {
userId: null,
organizationId: null,
},
fragments: {
user: () => Relay.QL`
fragment on User {
${CreateOrganizationMemberMutation.getFragment('user')},
}
`,
organization: () => Relay.QL`
fragment on Organization {
id,
${CreateOrganizationMemberMutation.getFragment('organization')},
}
`,
},
});
JoinOrganizationQueries.js
import Relay from 'react-relay';
export default {
user: () => Relay.QL`
query { userByRowId(rowId: $userId) }
`,
organization: () => Relay.QL`
query { organizationByRowId(rowId: $organizationId) }
`,
};
One unexpected benefit of doing things this way is that there is now a shareable url that can be used as an invite link for joining an organization in this app. If the user is logged in and goes to the link: <host>/profile/join-organization/<organizationRowId>, the mutation will run that joins the person as a member. In this use case, router.state.isAdmin is false, so the new membership will be disabled as an admin.

Resources