GraphQL - When to use a resolver or an argument with recursive and normalized data? - graphql

I'm working with a very large normalized and recursive object. I want to get the list of all recursive items. Should I use an argument or a custom resolver?
My object looks like:
{
products: [{
product_id: "car",
bundle_id: 5
},{
product_id: "door"
bundle_id: 6
},
{ product_id: "wheel" },
{ product_id: "metal" },
{ product_id: "glass" }],
bundles: [{
bundle_id: 5,
options: [{product_id: "door"},{product_id: "wheel"}]
},
{
bundle_id: 6,
options: [{product_id: "metal"},{product_id: "glass"}]
}]
}
You might notice that "car" is a bundle that has a door and a wheel. "door" is also a bundle that has metal and glass. This structure could recurse indefinitely. That is, a bundle could have infinitely more bundle products underneath it.
I want to get a list of all products for a bundle (example: "car"). What is the best approach?
I see two options.
First Option - use a custom resolver, for example child_products that would recurse and resolve to a flat array of all children:
products(product_id: "car") {
product_id
bundle {
options {
product_id
}
}
child_products {
product_id
bundle {
options {
product_id
}
}
}
}
Second Option - use an argument that specifies including all children:
products(product_id: "car", include_children: true) {
product_id
bundle {
options {
product_id
}
}
}
I'm going to build a JS library that can take the array of products and options and build the nested structure. Please let me know what you think is the right way. Thanks!

You should not need an argument like include_children because a client's query will be sufficient to determine whether to include the nodes or not -- if a client doesn't need the nodes, it can simply omit the appropriate field.
Based on the provided JSON object, I would expect a schema that looks something like this:
type Query {
product(id: ID!): Product
}
type Product {
id: ID!
bundle: Bundle
}
type Bundle {
id: ID!
options: [Product!]!
}
which would let you make a query like:
query {
product(id: "car") {
id
bundle {
options {
id
bundle {
id
# and so on...
}
}
}
}
}
The actual depth of this query would be left up to the client's needs. Recursive type definitions like this do present a possible attack vector and so you should also look into using a library like graphql-depth-limit or graphql-query-complexity.

Related

Gatsby's mapping between markdown files

I'm creating a multi-author site (using gatsby-plugin-mdx) and have the following file structure:
/posts
- /post-1/index.mdx
- /post-2/index.mdx
- ...
/members
- /member-a/index.mdx
- /member-b/index.mdx
- ...
In the frontmatter of the post page I have an array of authors like
authors: [Member A, Member B]
and I have the name of the author in the frontmatter of the author's markdown file.
I'd like to set the schema up so that when I query the post, I also get the details of the authors as well (name, email, etc.).
From reading this page it seems like I need to create a custom resolver... but all the examples I see have all the authors in one json file (so you have two collections, MarkdownRemark and AuthorJson... while I think for my case all my posts and members are in MarkdownRemark collection.
Thanks so much!
I end up doing something like this. Surely there's a cleaner way, but it works for me. It goes through all the Mdx and add a field called authors, which is queried, to all Mdx types.
One problem with this is that there's also authors under members, which is not ideal. A better approach is to define new types and change Mdx in the last resolver to your new post data type. Not sure how to get that to work though. At the end, I could query something like:
query MyQuery {
posts {
frontmatter {
title
subtitle
}
authors {
frontmatter {
name
email
}
}
}
}
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Mdx: {
authors: {
type: ["Mdx"],
resolve(source, args, context, info) {
return context.nodeModel.runQuery({
query: {
filter: {
fields: {
collection: { eq: "members" }
},
frontmatter: {
memberid: { in: source.frontmatter.authors },
},
},
},
type: "Mdx",
firstOnly: false,
})
}
}
},
}
createResolvers(resolvers)
}

Unable to filter custom data in siteMetaData in Gatsby using GraphQL in GraphiQL

I've created a basic Gatsby site with the default starter. I'm now trying to add some custom data (the people array) to gatsby-config.json like so:
module.exports = {
siteMetadata: {
title: `Gatsby Default Starter`,
description: `XXX`,
author: `#gatsbyjs`,
people : [
{ id : 1234, name : "Bill Smith", sales : 143, birthdate : "2233-03-22" },
{ id : 5678, name : "Roger Miller", sales : 281, birthdate : "2230-01-06" }
]
},
plugins: [
`gatsby-plugin-react-helmet`,
{ resolve: `gatsby-source-filesystem`,
options: { name: `images`, path: `${__dirname}/src/images`, }
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{ resolve: `gatsby-plugin-manifest`,
options: {
name: `gatsby-starter-default`, short_name: `starter`, start_url: `/`,
background_color: `#663399`, theme_color: `#663399`, display: `minimal-ui`,
icon: `src/images/gatsby-icon.png`
}
}
]
}
Then, in GraphiQL, what I'm trying to do is a query to get a list of people, but limit it to just those with sales above 200, that's my end goal. So first, I did a basic test:
{
site {
siteMetadata {
people {
name
}
}
}
}
That works, I get all people back. Then, I tried:
{
site {
siteMetadata {
people(sales: { gt: 200 }) {
name
}
}
}
}
That gets me an error "Unknown argument sales on field people of type SiteSiteMetadata". That kinda seems to be telling me that Sift underneath Gatsby doesn't have any notion of my custom fields in its schema, which would kind of make sense to me. So, as a test, I try this:
{
site {
siteMetadata(author: { eq: "none" }) {
author
title
}
}
}
My expectation is the query runs successfully but returns an empty result set since the author element's value isn't "none". But instead, I get the same basic error but now telling me "Unknown argument author on field siteMetadata of type Site" and now I'm confused because it seems like it should know about THOSE fields even if it doesn't know about arbitrary ones I add. Then again, maybe that query won't ever work because there's only a single siteMetaData object versus trying to query an array. I'm not sure.
So then I do some research and I see some reference to 'filter', so I try this now:
{
site {
siteMetadata(filter: { eq: "none" }) {
author
title
}
}
}
That gets me "Unknown argument filter on field siteMetadata of type Site."
And now I'm kind of out of ideas.
I did find one post that seemed to possibly imply that you can't query custom data elements like this at all, but some replies seem to imply you, in fact, can (and clearly that first test worked, so the data is found, I just can't get the filtering to work). Maybe I'm using the wrong syntax, but if so then I can't seem to find what the correct syntax looks like (and what's worse is that in the Gatsby docs, the ONE example that MIGHT provide me an answer is error'ing out in the playground and I can't see the code).
It seems like such a simple thing, but I'm at a loss. Any help would be greatly appreciated. Thanks!
EDIT: After I wrote this, I tried putting the data in a separate file that get loaded with the gatsby-transformer-json plugin and tried to query that. The data gets loaded, but I still can't filter the query. I can do:
{
testData {
people {
name
sales
}
}
}
...and that works, returns my data fine. But if I try:
{
testData {
people(sales:{gt:200}) {
name
sales
}
}
}
...or...
{
testData {
people(filter:{sales:{gt:200}}) {
name
sales
}
}
}
...I get the same types of errors. So, I think that at least proves this isn't an issue of querying it from siteMetaData specifically, but I still don't know how to make it do what I want.
For anyone who wants to reproduce this, just add the file data.json in the root of the project with this content:
{
"people" : [
{ "id" : 1234, "name" : "Bill Smith", "sales" : 143, "birthdate" : "2233-03-22" },
{ "id" : 5678, "name" : "Roger Miller", "sales" : 281, "birthdate" : "2230-01-06" }
]
}
Then, add this to the plugins array in gatsby-config.json:
{
resolve: `gatsby-transformer-json`,
options: { typeName: `testData` }
},
{
resolve: `gatsby-source-filesystem`,
options: { name: `data`, path: `${__dirname}/data.json` }
}
No other changes from the initially-generated project are needed. Then, just hop into GraphiQL and try to execute the queries above.
Or, to make things easier, I've created a codesandbox instance that demonstrates this:
https://codesandbox.io/s/gatsby-graphql-querying-json-issue-47km4
EDIT2: I had the thought that maybe this is an issue with GraphiQL itself. So, I created a simple component:
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
const Test = () => {
const testData = useStaticQuery(graphql`
query TestDateQuery {
testData(filter: {sales: {gte:200}}) {
people {
name
}
}
}
`)
console.log("testData", testData);
return (
<div />
)
}
export default Test
I then dropped that into my main Layout component. I get the same sort of error (about filter being an unknown argument) rather than seeing my data. If I remove the filter, I DO see ALL the data. So, again, I can't figure out why just filter isn't working, but that's what I've narrowed it down to.

How to adapt query to API?

I'm trying to wrap my head around GraphQL.
Right now I'm just playing with the public API of Artsy (an art website, playground at https://metaphysics-production.artsy.net). What I want to achieve is following:
I want to get all node types entities without declaring them by hand (is there a shortcut for this)?
I want every node with a field type from which I can read the type, without parsing through imageUrl etc. to fint that out.
What I constructed as of right now is this:
{
search(query: "Berlin", first: 100, page: 1, entities: [ARTIST, ARTWORK, ARTICLE]) {
edges {
node {
displayLabel
imageUrl
href
}
}
}}
Very primitive I guess. Can you guys help me?
TL;DR:
1) There is no shortcut, it's not something GraphQL offers out of the box. Nor is it something I was able to find via their Schema.
2) Their returned node of type Searchable does not contain a property for type that you're looking for. But you can access it via the ... on SearchableItem (union) syntax.
Explanation:
For question 1):
Looking at their schema, you can see that their search query has the following type details:
search(
query: String!
entities: [SearchEntity]
mode: SearchMode
aggregations: [SearchAggregation]
page: Int
after: String
first: Int
before: String
last: Int
): SearchableConnection
The query accepts an entities property of type SearchEntity which looks like this:
enum SearchEntity {
ARTIST
ARTWORK
ARTICLE
CITY
COLLECTION
FAIR
FEATURE
GALLERY
GENE
INSTITUTION
PROFILE
SALE
SHOW
TAG
}
Depending on what your usecase is, if you're constructing this query via some code, then you can find out which SearchEntity values they have:
{
__type(name: "SearchEntity") {
name
enumValues {
name
}
}
}
Which returns:
{
"data": {
"__type": {
"name": "SearchEntity",
"enumValues": [
{
"name": "ARTIST"
},
{
"name": "ARTWORK"
},
...
}
}
}
then store them in an array, omit the quotation marks from the enum and pass the array back to the original query directly as an argument.
Something along the lines of this:
query search($entities: [SearchEntity]) {
search(query: "Berlin", first: 100, page: 1, entities: $entities) {
edges {
node {
displayLabel
imageUrl
href
}
}
}
}
and in your query variables section, you just need to add:
{
"entities": [ARTIST, ARTWORK, ...]
}
As for question 2)
The query itself returns a SearchableConnection object.
type SearchableConnection {
pageInfo: PageInfo!
edges: [SearchableEdge]
pageCursors: PageCursors
totalCount: Int
aggregations: [SearchAggregationResults]
}
Digging deeper, we can see that they have edges, of type SearchableEdge - which is what you're querying.
type SearchableEdge {
node: Searchable
cursor: String!
}
and finally, node of type Searchable which contains the data you're trying to access.
Now, the type Searchable doesn't contain type:
type Searchable {
displayLabel: String
imageUrl: String
href: String
}
But, if you look at where that Searchable type is implemented, you can see SearchableItem - which contains the property of displayType - which doesn't actually exist in Searchable.
You can access the property of SearchableItem and get the displayType, like so:
{
search(query: "Berlin", first: 100, page: 1, entities: [ARTIST, ARTWORK, ARTICLE]) {
edges {
node {
displayLabel
imageUrl
href
... on SearchableItem {
displayType
}
}
}
}
}
and your result will look like this:
{
"data": {
"search": {
"edges": [
{
"node": {
"displayLabel": "Boris Berlin",
"imageUrl": "https://d32dm0rphc51dk.cloudfront.net/CRxSPNyhHKDIonwLKIVmIA/square.jpg",
"href": "/artist/boris-berlin",
"displayType": "Artist"
}
},
...

Use GraphQL to retrieve an object that contains an array of objects with different schemas

I am trying to write a query to retrieve an object with the property linkedCards that contains an array of objects with different schemas.
I have 3 different schemas (built in Contentful):
CardA example:
{
id: 42,
productName: 'Laptop',
price: 999
}
CardB example:
{
id: 999,
title: 'Buy our refurbished Laptops today!'
}
CardC example:
{
id: 100,
linkedCards: [
{
id: 42,
productName: 'Laptop',
price: 999
},
{
id: 999,
title: 'Buy our refurbished Laptops today!'
}
]
}
Query:
allCardC() {
nodes {
linkedCards {
id
title
}
}
}
When I try to run the following GraphQL query I get
"Cannot query field "title" on type "CardACardBUnion". Did you mean to use an inline fragment on "CardA" or "CardB"?"
Is there a built-in way to do this or can I use the ids of CardA & CardB somehow? Perhaps have one query to get the ids of the cards in linkedCards and another query to get said cards?
As the error indicates, you need to use an inline fragment when querying a field that resolves to a union:
allCardC {
nodes {
linkedCards {
... on CardA {
id
productName
price
}
... on CardB {
id
title
}
}
}
}
Fragments can be defined inline within a selection set. This is done to conditionally include fields based on their runtime type.
Unlike interfaces or regular object types, unions do not specify any particular fields, only the types that make up the union. That means a selection set for a field that returns a union must always use fragments to conditionally specify the fields depending on the actual type that the field resolves to.
It's like saying, "if this is the actual type of the returned object, request these fields".
You may find it useful to use a GraphQL interface to specify the fields that every card type has in common.
interface Card {
id: ID!
}
# type CardA implements Card { ... }
type CardB implements Card {
id: ID!
title: String!
}
type CardC implements Card {
id: ID!
linkedCards: [Card!]!
}
As #DanielRearden's answer suggests you still need to use (inline) fragments to select fields that are specific to one of the card types, but now that you know every card has an id field, you can select that directly.
allCardC {
nodes {
linkedCards {
id
... on CardB { title }
}
}
}

graphql using nested query arguments on parent or parent arguments on nested query

I have a product and items
Product:
{
id: Int
style_id: Int
items: [items]
}
Items:
{
id: Int
product_id: Int
size: String
}
I want to query products but only get back products that have an item with a size.
So a query could look like this:
products(size: ["S","M"]) {
id
style_id
items(size: ["S","M"]) {
id
size
}
}
But it seems like there should be a way where I can just do
products {
id
style_id
items(size: ["S","M"]) {
id
size
}
}
And in the resolver for the products I can grab arguments from the nested query and use them. In this case add the check to only return products that have those sizes. This way I have the top level returned with pagination correct instead of a lot of empty products.
Is this possible or atleast doing it the other way around:
products(size: ["S","M"]) {
id
style_id
items {
id
size
}
}
And sending the size argument down to the items resolver? Only way I know would be through context but the one place I found this they said that it is not a great idea because context spans the full query in all depths.
I agree with #DenisCappelini's answer. If possible, you can create a new type which represents only Products that have an Item.
However, if you don't want to do that, or if you're just interested in general about how a top-level selector can know about arguments on child selectors, here is a way to do that:
There are 2 ways to do it.
To do this:
products {
id
style_id
items(size: ["S","M"]) {
id
size
}
}
In graphql, resolvers have this signature:
(obj, args, context, info) => {}
The 4th argument, info, contains information about the entire request. Namely, it knows about arguments on the child selectors.
Use this package, or a similar one because there are others, to parse info for you: https://www.npmjs.com/package/graphql-parse-resolve-info
The above is quite a lot of work, so if you want to do this instead:
products(size: ["S","M"]) {
id
style_id
items {
id
size
}
}
Then in your resolver for products, you need to also return size.
Suppose this is your resolver for products:
(parent, args) => {
...
return {
id: '',
style_id: ''
}
}
Modify your resolver to also return size like this:
(parent, args) => {
...
return {
id: '',
style_id: '',
size: ["S", "M"]
}
}
Now in your resolve for products.items, you will have access to the size, like this:
(product, args) => {
const size = product.size
}
I found this useful #reference
//the typedef:
type Post {
_id: String
title: String
private: Boolean
author(username: String): Author
}
//the resolver:
Post: {
author(post, {username}){
//response
},
}
// usage
{
posts(private: true){
_id,
title,
author(username: "theara"){
_id,
username
}
}
}
IMO you should have a ProductFilterInputType which is represented by a GraphQLList(GraphQLString), and this resolver filters the products based on this list.
import { GraphQLList, GraphQLString } from 'graphql';
const ProductFilterInputType = new GraphQLInputObjectType({
name: 'ProductFilter',
fields: () => ({
size: {
type: GraphQLList(GraphQLString),
description: 'list of sizes',
}
}),
});
Hope it helps :)
these are few tweaks you can add and make your design better and also filter items properly.
1- change your product schema:
{
id: Int! # i would rather to use uuid which its type is String in gql.
styleId: Int
items: [items!] # list can be optional but if is not, better have item. but better design is below:
items(after: String, before: String, first: Int, last: Int, filter: ItemsFilterInput, orderBy: [ItemsOrderInput]): ItemsConnection
}
2- have a enum type for sizes:
enum Size {
SMALL
MEDIUM
}
3- change item schema
{
id: Int!
size: Size
productId: Int
product: Product # you need to resolve this if you want to get product from item.productId
}
4- have a filter type
input ItemFilterInput {
and: [ItemFilterInput!]
or: [ItemFilterInput!]
id: Int # you can use same for parent id like productId
idIn: [Int!]
idNot: Int
idNotIn: [Int!]
size: Size
sizeIn: [Size!]
sizeNotIn: [Size!]
sizeGt: Size # since sizes are not in alphabetic order and not sortable this wont be meaningful, but i keep it here to be used for other attributes. or you can also trick to add a number before size enums line 1SMALL, 2MEDIUM.
sizeGte: Size
sizeLt: Size
sizeLte: Size
sizeBetween: [Size!, Size!]
}
5- then create your resolvers to resolve the below query:
{
product {
items(filter: {sizeIn:[SMALL, MEDIUM]}) {
id
}
}
}
# if returning `ItemsConnection` resolve it this way:
{
product {
id
items {
edges {
node { # node will be an item.
id
size
}
}
}
}
}
Relay has a very good guideline to design a better schema.
https://relay.dev/
I also recommend you to add edges and node and connection to your resolvers to be able to add cursors as well. having product {items:[item]} will limit your flexibility.

Resources