13

Overview (simplified):

In my NodeJS server I've implemented the following GraphQL schema:

type Item {
  name: String,
  value: Float
}


type Query {
  items(names: [String]!): [Item]
}

The client query then passes an array of names, as an argument:

{
  items(names: ["total","active"] ) {
    name
    value
  }
}

The backend API queries a mysql DB, for the "total" and "active" fields (columns on my DB table) and reduces the response like so:

[{"name":"total" , value:100} , {"name":"active" , value:50}]

I would like my graphQL API to support "ratio" Item, I.E: I would like to send the following query:

{
  items(names: ["ratio"] ) {
    name
    value
  }
}

or

{
  items(names: ["total","active","ratio"] ) {
    name
    value
  }
}

And return active / total as the calculated result of that new field ([{"name":"ratio" , value:0.5}]). What would be a generic way to handle the "ratio" field differently?

Should it be a new type in my schema or should I implement the logic in the reducer?

Shlomi Schwartz
  • 11,238
  • 25
  • 93
  • 155
  • Whoud you like to return "ratio" in addition to "total" and "active" or return just "ratio"? – Andrija Ćeranić Nov 16 '17 at 14:34
  • It depends on the Query, but theoretically I would like to be able to get it all in a single query. `items(names: ["total","active","ratio"] )` – Shlomi Schwartz Nov 16 '17 at 14:36
  • Why not calculate the ratio in backend API when you get your db results. Then just return it as third name/value pair in addition to total and active. – Andrija Ćeranić Nov 16 '17 at 14:41
  • At the moment the backend receives the names and builds a DB query. if I want to calculate the results there, then I should check if one of the names is "ratio" remove it from the generic DB query, and when I have the results I can calculate the value ... Since I'm new to graphQL I was thinking there must be a more generic way to handle this scenario. – Shlomi Schwartz Nov 16 '17 at 14:45
  • You can make a new type, e.g. `type Kpi {total: Int, active: Int, ratio: Float}` and make the same input type. Then you can pass that in your query but as calculating is concerned I think it must be done in your resolver functions and GraphQL has no way of doing it. – Andrija Ćeranić Nov 16 '17 at 14:52
  • you could map your calculated items, I.E `("ratio" == "active"/"total")` so when you ask for "ratio" the DB query will request active + total and then will run a post processor for the calculations. – Roni Gadot Nov 20 '17 at 08:51
  • 1
    So where would you place this logic, in the reducer? – Shlomi Schwartz Nov 20 '17 at 08:54

2 Answers2

5

Joe's answer (append {"name":"ratio" , value:data.active/data.total} to the result once the result is fetched from database) would do it without making any schema changes.

As an alternative method or as a more elegant way to do it in GraphQL, the field names can be specified in the type itself instead of passing them as arguments. And compute ratio by writing a resolver.

So, the GraphQL schema would be:

Item {
  total: Int,
  active: Int,
  ratio: Float
}

type Query {
  items: [Item]
}

The client specifies the fields:

{
  items {
    total 
    active 
    ratio
  }
}

And ratio can be calculated inside the resolver.

Here is the code:

const express = require('express');
const graphqlHTTP = require('express-graphql');
const { graphql } = require('graphql');
const { makeExecutableSchema } = require('graphql-tools');
const getFieldNames = require('graphql-list-fields');

const typeDefs = `
type Item {
  total: Int,
  active: Int,
  ratio: Float
}

type Query {
  items: [Item]
}
`;

const resolvers = {
  Query: {
    items(obj, args, context, info) {
      const fields = getFieldNames(info) // get the array of field names specified by the client
      return context.db.getItems(fields)
    }
  },
  Item: {
    ratio: (obj) => obj.active / obj.total // resolver for finding ratio
  }
};

const schema = makeExecutableSchema({ typeDefs, resolvers });

const db = {
  getItems: (fields) => // table.select(fields)
    [{total: 10, active: 5},{total: 5, active: 5},{total: 15, active: 5}] // dummy data
}
graphql(
  schema, 
  `query{
    items{
      total,
      active,
      ratio
    }
  }`, 
  {}, // rootValue
  { db } // context
).then(data => console.log(JSON.stringify(data)))
Bless
  • 4,071
  • 2
  • 35
  • 39
2

You could set your resolver function up so it uses the second parameter - the arguments - to see if the name "ratio" is in your names array:

resolve: (root, { names }, context, fieldASTs) => {
    let arrayOfItems;
    // Contact DB, populate arrayOfItems with your total / active items

    // if 'ratio' is within your name array argument, calculate it:
    if (names.indexOf("ratio") > -1){
        // Calculate ratio
        arrayOfItems.push({ name: "ratio", value: calculatedRatio });
    }

    return(arrayOfItems);
}

I hope I understood your question correctly

Joe Wearing
  • 108
  • 1
  • 9