How to get a full aggregate for a connection using prisma-bindings

prisma

#1

Earlier, we were able to very simply get the full count for a connection in our pageInfo aggregate, but since this change we only get the size of the returned list for the aggregate.count field.

Besides polluting all our list resolvers with a new field that returns the full count, how could we go about augmenting our returned pageInfo with a fullCount field, while

  1. keeping DRY, and not having to implement custom logic for all resolvers that return lists and take pagination args
  2. still being able to pass the client’s info object to Prisma
  3. Not hit the DB an extra time for the count, it should be included in the ‘regular’ request

I’m imagining something like a client query a la

{
  users(first: 20){
    aggregate{
      count // returns 20
    }
    fullCount // returns total number of users
  }
}

where fullCount would resolve to a Prisma query like

{
  [type]Connection{
    aggregate{
      count
    }
  }
}

If there’s an other way to go about it, I’m open to suggestions :slight_smile:


#2

I’ve been wondering the same thing.
How did you manage to get the connections working?


#3

This is how I do this:

query EmployeePagination ($first: Int! $skip: Int! $orderBy: EmployeeOrderByInput $where: EmployeeWhereInput) {
  meta: employeesConnection (where: $where) {
    aggregate {
      count
    }
  }
  data: employees (first: $first skip: $skip orderBy: $orderBy where: $where) {
    id
    name
    # other fields...
  }
}

#4

@BenoitRanque - How would that work with bindings?

Let’s say I have the client query

{
  employees(first: 20){
    aggregate{
      count // I want this to return the full length of the `employees` list
    }
    edges { ... }
  }
}

what Prisma query using prisma-bindings will allow me to pass that query info and get the right result? Is there any way to match the client aggregate.count to the Prisma meta.aggregate.count using bindings?


#5

GraphQL supports aliases. So really this is not bindings specific.

{
  employees(first: 20){
    aggregate{
      count // this is the count of the employees that match the query, so obviously only the first 20
    }
    edges { ... }
  }
  employeesAliasCanBeWhatever: employees {
    aggregate{
      count // this is the count of all employees because we have not filter.
    }
  }
}

If you want to map this to other fields, you would need to await the results of the query before mapping them to other fields.

Edit:

You could also specify a resolver for that field and handle all the logic there.


#6

@BenoitRanque - Ah, thanks, that makes sense. This leads me to a new problem, though, but that is specific to our app implementation.

We’ve set both a default and a max value for the first argument for all of our connection queries. The default is 10, and the max is 100. This is in order to prevent queries that overload the server/database because they query too many items, perhaps with nested relations.

This means that, for example, if our resolver looks like this:

...
query.userCommentsConnection({ where: { subject: { id: subjectId } }, first, after, orderBy }, info),

and we query with

{
  userComments(first:15, after:"someid", subjectId:""){
    aggregate{
      count
    }
    edges{
      node{
        id
      }
    }
  }
  fullCount: userComments(subjectId:""){
    aggregate{
      count
    }
  }
}

we will still get the page size in the fullCount.aggregate.count because first will never be allowed to be undefined.

One way to combat this issue is to use Apollo’s “Operation Registry”, which only allows specifically uploaded queries to be run against the server, which we’re working on implementing in our system.

However, far from all developers will be using Apollo Operation Registry, which means that Prisma users will either have to

  1. Allow the fetching of full lists on their server, which is error prone for API consumers/internal frontend developers in terms of performance, as well as an attack vector on the system.
  2. End up with a separate Prisma query to return the correct count, a la
  const regularQuery = query.userDetailsConnection(
    {
      first,
      after,
      where: {...},
    },
    info,
  );

  if (containsAggregateCountQuery(info)) {
    const aggregateQuery = query.userDetailsConnection(
      {
        where: {...},
      },
      '{ aggregate { count } }',
    );
    const result = await regularQuery;
    result.aggregate = (await aggregateQuery).aggregate;
    return result;
  } else {
    return regularQuery;
  }
};

function containsAggregateCountQuery(info: GraphQLResolveInfo): boolean {
  return info.fieldNodes.some(
    (node) =>
      node.kind === Kind.FIELD &&
      node.selectionSet !== undefined &&
      node.selectionSet.selections.some(
        (node2) =>
          node2.kind === Kind.FIELD &&
          node2.name.value === 'aggregate' &&
          node2.selectionSet !== undefined &&
          node2.selectionSet.selections.some((node3) => node3.kind === Kind.FIELD && node3.name.value === 'count'),
      ),
  );
}

Is there a third option that I’m missing in my assessment?


#7

The easiest workaround is to add a dedicated field, and give it it’s own resolver.

This will mean an additional query however, so not optimal performance wise. You could try something whith fragments but I’m not sure that is feasible.


#8

Thanks again @BenoitRanque .

I would argue, then, that the change that was made was perhaps not the best decision. Or at least, Prisma could support another field under aggregate that returns the full list count regardless of arguments. It would be awesome if someone from the Prisma team could comment on their reasoning behind the decision, and perhaps give guidance into what they propose is the best course of action, while keeping the sane defaults that we have in place to prevent monstrous queries being run against the server.


#9

This topic was automatically closed 45 days after the last reply. New replies are no longer allowed.