Hoist child object into parent - graphql

Is there any way to reach down into the attributes of an object returned by a query?
So imagine that I have an object defined as:
{
id: string
myCollection(someArgs):collection
}
where collection is defined as:
{
metaArg1: int,
metaArg2: int,
...
metaArgn: int,
theArray: [objectType]
}
Now, I have a query like:
query myQuery{
fetchData(){
id,
myCollection(someArgs){
theArray {
...some attribute list
}
}
}
}
Which will return something like this:
{
id: 'id'
myCollection: {
theArray: [{...}, {...}, {...}]
}
}
If I just want the content of theArray, is there any way I can hoist its value one level up?
Essentially, I want to end up with:
{
id: 'id'
myCollection: [{...}, {...}, {...}] // This would be the value previously inside theArray
}
Is there any way to rewrite myQuery to accomplish that?

Related

Dynamically create pages with Gatsby based on many Contentful references

I am currently using Gatsby's collection routes API to create pages for a simple blog with data coming from Contentful.
For example, creating a page for each blogpost category :
-- src/pages/categories/{contentfulBlogPost.category}.js
export const query = graphql`
query categoriesQuery($category: String = "") {
allContentfulBlogPost(filter: { category: { eq: $category } }) {
edges {
node {
title
category
description {
description
}
...
}
}
}
}
...
[React component mapping all blogposts from each category in a list]
...
This is working fine.
But now I would like to have multiple categories per blogpost, so I switched to Contentful's references, many content-type, which allows to have multiple entries for a field :
Now the result of my graphQL query on field category2 is an array of different categories for each blogpost :
Query :
query categoriesQuery {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
Output :
{
"data": {
"allContentfulBlogPost": {
"edges": [
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
},
{
"id": "568r9e48-t1i8-sx4t8-9742-cdf70c4ed789vtu",
"name": "Test2",
"slug": "test-2"
}
]
}
},
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
}
]
}
},
...
Now that categories are inside an array, I don't know how to :
write a query variable to filter categories names ;
use the slug field as a route to dynamically create the page.
For blogposts authors I was doing :
query authorsQuery($author__slug: String = "") {
allContentfulBlogPost(filter: { author: { slug: { eq: $author__slug } } }) {
edges {
node {
id
author {
slug
name
}
...
}
...
}
And creating pages with src/pages/authors/{contentfulBlogPost.author__slug}.js
I guess I'll have to use the createPages API instead.
You can achieve the result using the Filesystem API, something like this may work:
src/pages/category/{contentfulBlogPost.category2__name}.js
In this case, it seems that this approach may lead to some caveats, since you may potentially create duplicated pages with the same URL (slug) because the posts can contain multiple and repeated categories.
However, I think it's more succinct to use the createPages API as you said, keeping in mind that you will need to treat the categories to avoid duplicities because they are in a one-to-many relationship.
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
`)
let categories= { slugs: [], names: [] };
result.data.allContentfulBlogPost.edges.map(({node}))=> {
let { name, slug } = node.category2;
// make some checks if needed here
categories.slugs.push(slug);
categories.names.push(name);
return new Set(categories.slugs) && new Set(categories.names);
});
categories.slugs.forEach((category, index) => {
let name = categories.names[index];
createPage({
path: `category/${category}`,
component: path.resolve(`./src/templates/your-category-template.js`),
context: {
name
}
});
});
}
The code's quite self-explanatory. Basically you are defining an empty object (categories) that contains two arrays, slugs and names:
let categories= { slugs: [], names: [] };
After that, you only need to loop through the result of the query (result) and push the field values (name, slug, and others if needed) to the previous array, making the needed checks if you want (to avoid pushing empty values, or that matches some regular expression, etc) and return a new Set to remove the duplicates.
Then, you only need to loop through the slugs to create pages using createPage API and pass the needed data via context:
context: {
name
}
Because of redundancy, this is the same than doing:
context: {
name: name
}
So, in your template, you will get the name in pageContext props. Replace it with the slug if needed, depending on your situation and your use case, the approach is exactly the same.

graphql - Refer to other fields in mutation

I want to create 2 related objects, e.g. 1 Location and 1 Place where Place has a reference to Location like so:
type Location {
id: String
name: String
}
type Place {
id: String
locationId: String
}
Is it possible to do this with 1 mutation request? Currently I'm doing this with 2 separate mutation requests like below:
mutation ($locationName: String!) {
insert_Location(objects: {name: $locationName}) {
returning {
id
}
}
}
//in another request, use the id returned from the request above
mutation ($locationId: String!) {
insert_Place(objects: {locationId: $locationId}) {
returning {
id
}
}
}
I'm aware it's possible to have multiple fields in a mutation so I could create 2 Locations in 1 mutation request like below.
mutation ($locationName: String!) {
location1: insert_Location(objects: {name: $locationName}) {
returning {
id
}
}
location2: insert_Location(objects: {name: $locationName}) {
returning {
id
}
}
}
However if I wanted to do this to create 1 Location and 1 Place, is there a way to retrieve the created Location Id and pass it to the 2nd field to create the Place?
For future reference:
As #Xetera pointed out, because the 2 types have a foreign key relationship you can do a nested insert mutation where hasura would handle setting the foreign key value. In my case it would look something like:
mutation ($locationName: String!) {
insert_Place(
objects: {
Location: {data: {name: $locationName}}, //hasura will create Location and assign the id to Place.locationId
}
) {
returning {
id
}
}
}
Docs here for further reading: https://hasura.io/docs/1.0/graphql/manual/mutations/insert.html#insert-an-object-along-with-its-related-objects-through-relationships

Change backend service based on GraphQL Param

I have a GraphQL Schema as such:
BigParent (parentParam: Boolean) {
id
name
Parent {
id
name
Child (childParam: Boolean) {
id
name
}
}
}
How can I write resolvers such that I call different backend APIs based on whether the parentParam is true or the childParam is true? The first option is straight-forward. The second one needs to kind of reconstruct the Graph based on the values returned by the service data returned at the level of Child.
I'm not considering both the options as true, as I'll assign some priority so that the param at child level is not considered while the param at the parent level is passed.
You can get the arguments for any field selection in the query by traversing the GraphQL resolver info parameter.
Assuming your query looks like this:
query {
BigParent(parentParam: Boolean) {
id
name
Parent {
id
name
Child(childParam: Boolean) {
id
name
}
}
}
}
You should be able to do something like this:
function getChildParam(info) {
// TODO: Return 'childParam'
}
const resolvers = {
Query: {
async BigParent(parent, args, context, info) {
// 'args' contains the 'parentParam' argument
const parentParam = args.parentParam;
// Extract the 'childParam' argument from the info object
const childParam = getChildParam(info);
if (parentParam || childParam) {
return context.datasource1.getData();
}
return context.datasource2.getData();
},
},
};

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.

How to write resolvers for GraphQL Mutation fields

My GraphQL schema looks like this:
type Outer {
id: ID
name: String,
inner: [Inner]
}
type Inner {
id: ID
name: String
}
input OuterInput {
name: String,
inner: InnerInput
}
Input InnerInput {
name: String
}
type Query {
getOuter(id: ID): Outer
}
type Mutation {
createOuter(outer: OuterInput): Outer
}
In database, Outer object is store this way:
{
id: 1,
name: ‘test’,
inner: [5, 6] //IDs of inner objects. Not the entire inner object
}
Outer and Inner are stored in separate database collections/tables (I’m using DynamoDB)
My resolvers look like this:
Query: {
getOuter: (id) => {
//return ‘Outer’ object with the ‘inner’ IDs from the database since its stored that way
}
}
Outer: {
inner: (outer) => {
//return ‘Inner’ objects with ids from ‘outer.inner’ is returned
}
}
Mutation: {
createOuter: (outer) => {
//extract out the IDs out of the ‘inner’ field in ‘outer’ and store in DB.
}
}
OuterInput: {
inner: (outer) => {
//Take the full ‘Inner’ objects from ‘outer’ and store in DB.
// This is the cause of the error
}
}
But I’m not able to write a resolver for the ‘inner’ field in ‘OuterInput’ similar to what I did for ‘Outer’. GraphQL throws an error saying
Error: OuterInput was defined in resolvers, but it's not an object
How can I handle such a scenario if writing resolvers for Input types are not allowed?
Resolvers are not necessary for input types. Input types are only passed in as values to field arguments, which means their value is already know -- it does not need to be resolved by the server, unlike values for fields being returned in the query.
The signature for every resolver is the same and includes 4 parameters: 1) the parent object, 2) the arguments for the field being resolved, 3) the context and 4) an object containing more detailed information about the request as a whole.
So to access the outer argument for your createOuter mutation:
Mutation: {
createOuter: (root, args, ctx, info) => {
console.log('Outer: ', args.outer)
console.log('Outer name: ', args.outer.name)
console.log('Inner: ', args.outer.inner)
console.log('Inner name: ', args.outer.inner.name)
// resolver logic here
}
}

Resources