GRANDstack starter - GraphQL Schema - Can't run a subquery

I have been working with the most recent GRANDstack starter application. I have gotten hung up on how to create a more complicated traversal as shown here: https://grandstack.io/docs/neo4j-graphql-js.html

type Movie {
  movieId: ID!
  title: String
  year: Int
  plot: String
  poster: String
  imdbRating: Float
  similar(first: Int = 3, offset: Int = 0): [Movie] @cypher(statement: "MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie) RETURN o")
  degree: Int @cypher(statement: "RETURN SIZE((this)-->())")
  actors(first: Int = 3, offset: Int = 0): [Actor] @relation(name: "ACTED_IN", direction:"IN")
}

type Actor {
  id: ID!
  name: String
  movies: [Movie]
}

type Query {
  Movie(id: ID, title: String, year: Int, imdbRating: Float, first: Int, offset: Int): [Movie]
}
';

and here

{
  Movie(title: "River Runs Through It, A") {
    title
    year
    imdbRating
    actors {
      name
    }
    similar(first: 3) {
      title
    }
  }
}

When attempting to use a similar step as shown by actors I am failing to pull in the correct data associated. I am using some of my own data I have been inserting into the GRANDstack starter application, but the concept is transferable. What are the caveats and key concepts that I need to consider to make a similar traversal?

For use in the GRANDstack application, how would I need to reference that "subquery" within the context of the starter application framework (i.e. my equivalent actors doesn't show up)?

Sorry, you have to provide more detail. Don't understand what you're looking for. Can you share your attempt, i.e. how far you've come and how it fails?

Below is what I have in graphql-schema.js:

import { neo4jgraphql } from "neo4j-graphql-js";

export const typeDefs = `
type User {
  id: ID!
  name: String
  friends(first: Int = 10, offset: Int = 0): [User] @relation(name: "FRIENDS", direction: "BOTH")
  reviews(first: Int = 10, offset: Int = 0): [Review] @relation(name: "WROTE", direction: "OUT")
  avgStars: Float @cypher(statement: "MATCH (this)-[:WROTE]->(r:Review) RETURN toFloat(avg(r.stars))")
  numReviews: Int @cypher(statement: "MATCH (this)-[:WROTE]->(r:Review) RETURN COUNT(r)")
  totalStars: Int @cypher(statement: "MATCH (this)-[:WROTE]->(r:Review) RETURN SUM(r.stars)")
}

type Business {
  id: ID!
  name: String
  address: String
  city: String
  state: String
  reviews(first: Int = 10, offset: Int = 0): [Review] @relation(name: "REVIEWS", direction: "IN")
  categories(first: Int = 10, offset: Int =0): [Category] @relation(name: "IN_CATEGORY", direction: "OUT")

  numReviews: Int @cypher(statement: "MATCH (this)<-[:REVIEWS]-(r:Review) RETURN count(r)")
  avgStars: Float @cypher(statement: "MATCH (this)<-[:REVIEWS]-(r:Review) WHERE r.stars IS NOT NULL RETURN toFloat(avg(r.stars))")
  category: String @cypher(statement: "MATCH (this)-[:IN_CATEGORY]->(r:Category) RETURN r.name")
}

type Review {
  id: ID!
  stars: Int
  text: String
  business: Business @relation(name: "REVIEWS", direction: "OUT")
  user: User @relation(name: "WROTE", direction: "IN")

  business_name: String @cypher(statement: "MATCH (this)-[:REVIEWS]->(r:Business) RETURN r.name")
  user_name: String @cypher(statement: "MATCH (this)<-[:WROTE]-(r:User) RETURN r.name")
}

type Category {
  name: ID!
  businesses(first: Int = 10, offset: Int = 0): [Business] @relation(name: "IN_CATEGORY", direction: "IN")
}

type Ring {
  id: ID!
  name: String
  divisions(first: Int = 10, offset: Int = 0): [Division] @relation(name: "OCCURS_IN", direction: "IN")
}

type Division {
  id: ID!
  name: String
  div: String
  sex: String
  rank: String
  ageLow: Int
  ageHigh: Int
  status: String
}

type Person {
  id: ID!
  name: String
  sex: String
  age: Int
  rank: String   
}

type Query {
    users(id: ID, name: String, first: Int = 10, offset: Int = 0): [User]
    businesses(id: ID, name: String, first: Int = 10, offset: Int = 0): [Business]
    reviews(id: ID, stars: Int, first: Int = 10, offset: Int = 0): [Review]
    category(name: ID!): Category
    usersBySubstring(substring: String, first: Int = 10, offset: Int = 0): [User] @cypher(statement: "MATCH (u:User) WHERE u.name CONTAINS $substring RETURN u")

    divisions(id: ID, name: String, div: String, sex: String, rank: String, ageLow: Int, ageHigh: Int, status: String, first: Int, offset: Int): [Division]
    rings(id: ID, name: String, first: Int = 10, offset: Int = 0): [Ring]
    competitors(name: String): [Person]
    judges(name: String): [Person] @cypher(statement: "MATCH (p:Person) WHERE p.age >=18 AND p.rank CONTAINS 'Black Belt' RETURN p")
}
`;

export const resolvers = {
  Query: {
    users: neo4jgraphql,
    businesses: neo4jgraphql,
    reviews: neo4jgraphql,
    category: neo4jgraphql,
    usersBySubstring: neo4jgraphql,

    divisions: neo4jgraphql,
    rings: neo4jgraphql,
    competitors: neo4jgraphql,
    judges: neo4jgraphql
  }
};

Following the same general format as UserList.js, 'BusinessList.js, etc., I createdRingList.js, in which I added a material card that expands to show a table. The card title is linked to the Queryringsin which I would like to query alldivisionsassociated with that ring. It is the "nested or subquery"divisionsthat I cannot seem to get working correctly. I have tried multiple things. As it is shown here now theandmaterial components are associated/mapped with theringquery to pull each ring name. Then in

I have to tried to map to thedivisions` query.
import React from "react";
import { Query } from "react-apollo";
import gql from "graphql-tag";
import "./RingList.css";
import { withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import Paper from "@material-ui/core/Paper";
import { TableSortLabel } from "@material-ui/core";

import classnames from 'classnames';
import Card from "@material-ui/core/Card";
import CardHeader from '@material-ui/core/CardHeader';
import CardContent from '@material-ui/core/CardContent';
import CardActions from "@material-ui/core/CardActions";
import IconButton from "@material-ui/core/IconButton";
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import Collapse from '@material-ui/core/Collapse';

const styles = theme => ({
  root: {
    // maxWidth: 700,
    marginTop: theme.spacing.unit * 3,
    overflowX: "auto",
    margin: "auto"
  },
  table: {
    minWidth: 700
  },

  card: {
    maxWidth: "auto",
  },

  expand: {
    transform: 'rotate(0deg)',
    transition: theme.transitions.create('transform', {
      duration: theme.transitions.duration.shortest,
    }),
    marginLeft: 'auto',
    [theme.breakpoints.up('sm')]: {
      marginRight: -8,
    },
  },
  expandOpen: {
    transform: 'rotate(180deg)',
  },
});

function getSorting(order, orderBy) {
  return order === "desc"
    ? (a, b) => (b[orderBy] < a[orderBy] ? -1 : 1)
    : (a, b) => (a[orderBy] < b[orderBy] ? -1 : 1);
}

class RingList extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      order: "asc",
      orderBy: "name"
    };
  }

  state = { expanded: false };

  handleExpandClick = () => {
    this.setState(state => ({ expanded: !state.expanded }));
  };

  handleSortRequest = property => {
    const orderBy = property;
    let order = "desc";

    if (this.state.orderBy === property && this.state.order === "desc") {
      order = "asc";
    }

    this.setState({ order, orderBy });
  };

  render() {
    const { order, orderBy } = this.state;
    return (
      <Query
        query={gql`
          {
            rings(first: 10, offset: 0) {
              id
              name
              divisions(first: 10, offset: 0) {
                id
                name
                div
                sex
                rank
                ageLow
                ageHigh
                status
              }
            }            
          }
        `}
      >
        {({ loading, error, data }) => {
          if (loading) return <p>Loading...</p>;
          if (error) return <p>Error</p>;

          return (
            <div >
            {data.rings
              .slice()
              .map(n => {
                return(
                  <Paper key={n.id} className={this.props.classes.root}>
                  {/*<Card key={n.name} className={this.props.classes.card}>*/}
                  <Card className={this.props.classes.card}>
                    <CardHeader
                      title={n.name}
                     />
                    <CardContent>
                      {n.name}
                    </CardContent>
                    <CardActions>
                      <IconButton
                        className={classnames(this.props.classes.expand, {
                          [this.props.classes.expandOpen]: this.state.expanded,
                        })}
                        onClick={this.handleExpandClick}
                        aria-expanded={this.state.expanded}
                        aria-label="Show more"
                      >
                        <ExpandMoreIcon />
                      </IconButton>
                    </CardActions>
                    <Collapse in={this.state.expanded} timeout="auto" unmountOnExit>
                      <CardContent>
                        <Table className={this.props.classes.table}>
                          <TableHead>
                            <TableRow>
                              <TableCell
                                key="name"
                                sortDirection={orderBy === "name" ? order : false}
                              >
                                <Tooltip
                                  title="Sort"
                                  placement="bottom-start"
                                  enterDelay={300}
                                >
                                  <TableSortLabel
                                    active={orderBy === "name"}
                                    direction={order}
                                    onClick={() => this.handleSortRequest("name")}
                                  >
                                    Name
                                  </TableSortLabel>
                                </Tooltip>
                              </TableCell>
                              <TableCell
                                key="div"
                                sortDirection={orderBy === "div" ? order : false}
                              >
                                <Tooltip
                                  title="Sort"
                                  placement="bottom-end"
                                  enterDelay={300}
                                >
                                  <TableSortLabel
                                    active={orderBy === "div"}
                                    direction={order}
                                    onClick={() => this.handleSortRequest("div")}
                                  >
                                    Div
                                  </TableSortLabel>
                                </Tooltip>
                              </TableCell>
                              <TableCell
                                key="sex"
                                sortDirection={orderBy === "sex" ? order : false}
                              >
                                <Tooltip
                                  title="Sort"
                                  placement="bottom-start"
                                  enterDelay={300}
                                >
                                  <TableSortLabel
                                    active={orderBy === "sex"}
                                    direction={order}
                                    onClick={() => this.handleSortRequest("sex")}
                                  >
                                    Sex
                                  </TableSortLabel>
                                </Tooltip>
                              </TableCell>
                              <TableCell
                                key="rank"
                                sortDirection={orderBy === "rank" ? order : false}
                              >
                                <Tooltip
                                  title="Sort"
                                  placement="bottom-start"
                                  enterDelay={300}
                                >
                                  <TableSortLabel
                                    active={orderBy === "rank"}
                                    direction={order}
                                    onClick={() => this.handleSortRequest("rank")}
                                  >
                                    Rank
                                  </TableSortLabel>
                                </Tooltip>
                              </TableCell>

                              <TableCell
                                key="ageLow"
                                sortDirection={orderBy === "ageLow" ? order : false}
                                numeric
                              >
                                <Tooltip
                                  title="Sort"
                                  placement="bottom-start"
                                  enterDelay={300}
                                >
                                  <TableSortLabel
                                    active={orderBy === "ageLow"}
                                    direction={order}
                                    onClick={() => this.handleSortRequest("ageLow")}
                                  >
                                    ageLow
                                  </TableSortLabel>
                                </Tooltip>
                              </TableCell>
                              <TableCell
                                key="ageHigh"
                                sortDirection={orderBy === "ageHigh" ? order : false}
                                numeric
                              >
                                <Tooltip
                                  title="Sort"
                                  placement="bottom-start"
                                  enterDelay={300}
                                >
                                  <TableSortLabel
                                    active={orderBy === "ageHigh"}
                                    direction={order}
                                    onClick={() => this.handleSortRequest("ageHigh")}
                                  >
                                    ageHigh
                                  </TableSortLabel>
                                </Tooltip>
                              </TableCell>
                              <TableCell
                                key="status"
                                sortDirection={orderBy === "status" ? order : false}
                                numeric
                              >
                                <Tooltip
                                  title="Sort"
                                  placement="bottom-start"
                                  enterDelay={300}
                                >
                                  <TableSortLabel
                                    active={orderBy === "status"}
                                    direction={order}
                                    onClick={() => this.handleSortRequest("status")}
                                  >
                                    Status
                                  </TableSortLabel>
                                </Tooltip>
                              </TableCell>
                            </TableRow>
                          </TableHead>
                          <TableBody>
                            {data.rings
                              .slice()
                              .sort(getSorting(order, orderBy))
                              .map(m => {
                                return (
                                  //<TableRow key={n.id}>
                                  //<TableRow key={n.divisions.name}>
                                  <TableRow key={m.divisions.id}>
                                    <TableCell component="th" scope="row">
                                      {m.divisions.name}
                                    </TableCell>
                                    <TableCell>{m.divisions.div}</TableCell>
                                    <TableCell>{m.divisions.sex}</TableCell>
                                    <TableCell>{m.divisions.rank}</TableCell>
                                    <TableCell numeric>{m.divisions.ageLow}</TableCell>
                                    <TableCell numeric>{m.divisions.ageHigh}</TableCell>
                                    <TableCell>{m.divisions.status}</TableCell>
                                  </TableRow>
                                );
                              })}
                          </TableBody>
                        </Table>
                      </CardContent>
                    </Collapse>
                  </Card>
                  </Paper>
                );
            })}
            </div>
          );
        }}
      </Query>
    );
  }
}

export default withStyles(styles)(RingList);

Hey Michael -

First, can you confirm the GraphQL query works and returns the expected results when running in GraphQL Playground? Just want to make sure the React component is getting the data it expects.

{
        rings(first: 10, offset: 0) {
          id
          name
          divisions(first: 10, offset: 0) {
            id
            name
            div
            sex
            rank
            ageLow
            ageHigh
            status
          }
        }            
      }

When running that query in the GraphQL playground I do get results that appear to match the expected query results. I am uncertain if I need to use a second "mapping" when populating the table, but even attempting using n.divisions.name doesn't appear to work since I have already mapped data.rings to n. So I have tried the adding the second mapping, but can't seem to figure out where it is getting tripped up.

data.rings is an array, so calling data.rings.map allows you to iterate over each element of the rings array (creating a table row for each element). Similarly, divisions is also an array, so you can map over the divisions array in a nested fashion as you are iterating over rings, and do some operation for each division (like adding a row for each division in the table). But calling n.divisions.name will throw an error because divisions is an array, not an object.

For example, something like this will log all division names for each ring:

data.rings.map( r => {
  console.log("Ring: " + r.name);
  r.divisions.map( d => {
    console.log("Division: " + d.name);
  })
})

You want to use a similar concept for adding table rows.

1 Like

Brilliant! That was what I needed. I was getting stuck on the mapping. I wasn't utilizing the variable for the data.rings when mapping divisions.

Thank you very much!

Cool - glad that helped!

1 Like

One piece of clarification that I still don't understand is the use of

(first: 10, offset: 0)

in the schema and when querying. Is this required, or can I choose to not specify these parameters?

You probably already solved that riddle ( the meaning of (first: 10, offset: 0)), but for the sake of other readers let's clarify it.
The first keyword means that at most 10 items are fetched.
With offset you determine where to start in the whole result set.

Read more about it

here.

2 Likes