In need of assistance with shaping GraphQL output?

Here is an example of a returned data object:

{
  data: {
    posts: {
      edges: [
        {
          post: {
            id: "1",
            title: "Foo"
          }
        },
        {
          post: {
            id: "2",
            title: "Bar"
          }
        }
      ]
    }
  }
}

The query used to obtain this data is as follows:

    query MyQuery {
      posts {
        edges {
          post: node {
            id
            title
          }
        }
      }
    }

While this structure works, it requires creating nested interfaces. Is there a way to simplify the results or transform them using JavaScript map() instead?

Ideally, I would prefer the response to be formatted like this:

{
  data: {
    posts: [
      {
        id: "1",
        title: "Foo"
      },
      {
        id: "2",
        title: "Bar"
      }
    ]
  }
}

Note: Any solution provided should be implemented on the client side since updating the server-side GraphQL schema is not an option.

Thank you!

EDIT

Below is my Angular/TypeScript code that handles the GraphQL requests and responses...

post.service.ts

import { Injectable } from '@angular/core';
...

model/post.ts

interface CategoryNode {
    ...
}

export interface Post {
    ...
}

As shown above, too many nested interfaces are required for the current implementation.

Actual sample response (used for unit testing)

      
{
  data: {
    posts: {
      edges : [
        {
          post: {
            id: "cG9zdDoxMjc=",
            title: "Lorem Ipsum",
            date: "2022-01-06T22:00:53",
            uri: "\/2022\/01\/06\/lorem-ipsum\/",
            categories: {
              edges: [
                {
                  category: {
                    id: "dGVybToy",
                    name: "General",
                    uri: "\/category\/general\/"
                  }
                }
              ]
            }
          },
          cursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
        },
        {
          post: {
            id: "cG9zdDoxMjc=",
            title: "Lorem Ipsum",
            date: "2022-01-06T22:00:53",
            uri: "\/2022\/01\/06\/lorem-ipsum\/",
            categories: {
              edges: [
                {
                  category: {
                    id: "dGVybToy",
                    name: "General",
                    uri: "\/category\/general\/"
                  }
                },
                {
                  category: {
                    id: "dGVybToy",
                    name: "General",
                    uri: "\/category\/general\/"
                  }
                }
              ]
            }
          },
          cursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
        },
      ],
      pageInfo: {
        startCursor: "YXJyYXljb25uZWN0aW9uOjEyNw==",
        hasPreviousPage: false,
        hasNextPage: false,
        endCursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
      }       
    }
  }
}; 

Answer №1

It's a challenge to provide advice on modifying your query to achieve the desired shape without being able to introspect the GQL schema. However, you can transform the response value into the desired shape by following this approach:

TS Playground

interface PostNode {
  id: string;
  title: string;
}

interface PostEdge { post: PostNode; }

type GQLResponse<T> = { data: T; };

type PostsResponse = GQLResponse<{
  posts: {
    edges: PostEdge[];
  };
}>;

type TransformedPostsResponse = {
  data: {
    posts: PostNode[];
  };
};

function transformPostsResponse (res: PostsResponse): TransformedPostsResponse {
  const result: TransformedPostsResponse = {data: {posts: []}};
  for (const edge of res.data.posts.edges) result.data.posts.push(edge.post);
  return result;
}

const postsResponse: PostsResponse = {
  data: {
    posts: {
      edges: [
        {
          post: {
            id: "1",
            title: "Foo"
          }
        },
        {
          post: {
            id: "2",
            title: "Bar"
          }
        }
      ]
    }
  }
};

const result = transformPostsResponse(postsResponse);
console.log(result);

Demo (compiled JS from the TS Playground):

"use strict";
function transformPostsResponse(res) {
    const result = { data: { posts: [] } };
    for (const edge of res.data.posts.edges)
        result.data.posts.push(edge.post);
    return result;
}
const postsResponse = {
    data: {
        posts: {
            edges: [
                {
                    post: {
                        id: "1",
                        title: "Foo"
                    }
                },
                {
                    post: {
                        id: "2",
                        title: "Bar"
                    }
                }
            ]
        }
    }
};
const result = transformPostsResponse(postsResponse);
console.log(result);

Answer №2

I utilized nested map() functions to transform the GraphQL response into a more structured object.

Here is my final code for reference in case anyone encounters a similar question or issue.

NOTE: The code below uses "articles" instead of "posts," but applies the same principles.

models/article-gql.ts

interface GqlCategoryNode {
    category: {
        id: string;
        name: string;
        uri: string;
    };
}

interface GqlArticleNode {
    article: {
        id: string;
        title: string;
        date: string;
        uri: string;
        categories: {
            edges: GqlCategoryNode[]
        };
    };
    cursor: string;
}

export interface GqlArticleResponse {
    edges: GqlArticleNode[]
    pageInfo: {
        startCursor: string
        hasPreviousPage: boolean
        hasNextPage: boolean
        endCursor: string
    }
}

models/article.ts

interface Category {
    id: string;
    name: string;
    uri: string;
}

export interface Article {
    id: string;
    title: string;
    date: string;
    uri: string;
    categories: Category[];
    cursor: string;
}

export interface PageInfo {
    startCursor: string;
    hasPreviousPage: boolean;
    hasNextPage: boolean;
    endCursor: string;
}

article.service.ts

import { Injectable } from '@angular/core';
import { Apollo, gql } from 'apollo-angular';
import { map, Observable } from 'rxjs';
import { GraphQLResponse } from 'src/app/core/types/graphQLResponse';
import { Article, PageInfo } from '../models/article';
import { GqlArticleResponse } from '../models/article-gql';

export const getArticlesQuery = gql`
  query getArticlesQuery {
    articles: posts {
      edges {
        article: node {
          id
          title
          date
          uri
          categories {
            edges {
              category: node {
                id
                name
                uri
              }
            }
          }
        }
        cursor
      }
      pageInfo {
        startCursor
        hasPreviousPage
        hasNextPage
        endCursor
      }
    }
  }
`;

@Injectable({
  providedIn: 'root'
})
export class ArticleService {

  constructor(private apollo: Apollo) { }

  public getArticles(): Observable<[PageInfo, Article[]]> {
    return this.apollo.query<GraphQLResponse<'articles', GqlArticleResponse>>({
      query: getArticlesQuery
    }).pipe(map(resp => {
      return [
        resp.data.articles.pageInfo as PageInfo,
        resp.data.articles.edges.map((articleNode) => {
        return {
          id: articleNode.article.id,
          title: articleNode.article.title,
          date: articleNode.article.date,
          uri: articleNode.article.uri,
          cursor: articleNode.cursor,
          categories: articleNode.article.categories.edges.map((categoryNode) => {
            return {
              id: categoryNode.category.id,
              name: categoryNode.category.name,
              uri: categoryNode.category.uri
            }
          })
        }
      }]
    })) as Observable<[PageInfo, Article[]]>;
  }

}

article.service.spec.ts

In this section, I showcase how I transformed the server response within the service and verified the transformed response through testing.

import { TestBed } from '@angular/core/testing';
import { Apollo } from 'apollo-angular';
import { ApolloTestingController, ApolloTestingModule } from 'apollo-angular/testing';
import { Article, PageInfo } from '../models/article';
import { GqlArticleResponse } from '../models/article-gql';
import { ArticleService, getArticlesQuery } from './article.service';


describe('ArticleService', () => {
  let service: ArticleService;
  let controller: ApolloTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        ApolloTestingModule,
      ],
      providers: [
        ArticleService
      ]
    });
    service = TestBed.inject(ArticleService);
    controller = TestBed.inject(ApolloTestingController);
  });

  afterEach(async () => {
    const apolloClient = TestBed.inject(Apollo).client;
    await apolloClient.clearStore();
  })

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should return a list of articles', (done) => {
    const mockArticlesServerResponse: GqlArticleResponse = {
      edges: [
        {
          article: {
            id: "cG9zdDoxMjc=",
            title: "Lorem Ipsum",
            date: "2022-01-06T22:00:53",
            uri: "/2022/01/06/lorem-ipsum/",
            categories: {
              edges: [
                {
                  category: {
                    id: "dGVybToy",
                    name: "General",
                    uri: "/category/general/"
                  }
                }
              ]
            }
          },
          cursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
        },
        {
          article: {
            id: "cG9zdDoxMjc=",
            title: "Lorem Ipsum",
            date: "2022-01-06T22:00:53",
            uri: "/2022/01/06/lorem-ipsum/",
            categories: {
              edges: [
                {
                  category: {
                    id: "dGVybToy",
                    name: "General",
                    uri: "/category/general/"
                  }
                },
                {
                  category: {
                    id: "dGVybToy",
                    name: "Something",
                    uri: "/category/general/"
                  }
                }
              ]
            }
          },
          cursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
        }
      ],
      pageInfo: {
        startCursor: "YXJyYXljb25uZWN0aW9uOjEyNw==",
        hasPreviousPage: false,
        hasNextPage: false,
        endCursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
      }
    };

    const mockArticlesServiceResponse: [PageInfo, Article[]] = [
      {
        startCursor: "YXJyYXljb25uZWN0aW9uOjEyNw==",
        hasPreviousPage: false,
        hasNextPage: false,
        endCursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
      },
      [
        {
          id: "cG9zdDoxMjc=",
          title: "Lorem Ipsum",
          date: "2022-01-06T22:00:53",
          uri: "/2022/01/06/lorem-ipsum/",
          categories: [
            {
              id: "dGVybToy",
              name: "General",
              uri: "/category/general/"
            }
          ],
          cursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
        },
        {
          id: "cG9zdDoxMjc=",
          title: "Lorem Ipsum",
          date: "2022-01-06T22:00:53",
          uri: "/2022/01/06/lorem-ipsum/",
          categories: [
            {
              id: "dGVybToy",
              name: "General",
              uri: "/category/general/"
            },
            {
              id: "dGVybToy",
              name: "Something",
              uri: "/category/general/"
            }
          ],
          cursor: "YXJyYXljb25uZWN0aW9uOjEyNw=="
        }
      ]
    ];

    service.getArticles().subscribe(resp => {
      expect(resp).toEqual(mockArticlesServiceResponse);
      done();
    });

    const req = controller.expectOne(getArticlesQuery);
    expect(req.operation.operationName).toBe('getArticlesQuery');
    req.flush({ data: { articles: mockArticlesServerResponse } });
    controller.verify();

  });
});

A big thank you to everyone for their collaboration and support!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Using a function as a variable within jQuery's .click() method

Currently, I am utilizing a tabbed interface that is being managed by a jQuery function to handle the click events of my tabs. $(document).ready(function () { $('a#foo').click(function() { //content, various calls return fal ...

Adjust the width of the iframe

I've been tasked with customizing a customer's website, and the admin has given me permission to use my own JavaScript file (myScript.js). How can I manipulate the iframe width from 160px to 200px using DOM Operations? <div id="iframeDiv& ...

Connect the visibility of one object to the visibility of another object

How can I make DOM-element a visible when DOM-element b is visible, and hidden when b is hidden using jQuery? I'm seeking a solution to achieve this effect. Is there a way to accomplish this with jQuery? Thank you in advance! To clarify my question ...

Utilizing dropzone.js with asp.net Web Forms

Is it possible to retrieve the image properties (such as height and width) after uploading a file using dropzone.js in an .aspx application, in order to perform client-side animations? Javascript $(document).ready(function () { $(".dropzone").dropzon ...

Why aren't the JavaScript stars shining once I've selected one?

I recently set up a star rating feature on my website by following the tutorial "Creating an Ajaxified Star Rating System in Rails 3". Although I managed to get everything working on the index page, I noticed that the stars do not remain lit up after bein ...

Using Jquery to display text when the condition is not met

Here is the code snippet I am currently working with: http://jsfiddle.net/spadez/mn77f/6/ I am trying to make it display a message when there are no fields (questions) present. I attempted to achieve this by adding the following lines of code: } else ...

The step-by-step guide to testing an Angular promise using Jasmine

An issue arises when attempting to test the code below using Jasmine, as the console.log in `then` is never called. The question remains: is this problem related to Angular or Jasmine? describe("angular test", function() { var $q; ...

What are the differences between ajax loaded content and traditional content loading?

Can you explain the distinction between the DOM loaded by AJAX and the existing DOM of a webpage? Is it possible to load all parts of an HTML page via AJAX without any discrepancies compared to the existing content, including meta tags, scripts, styles, e ...

What is the best way to access a variable from outside a function in Node.js?

app.get('/:id', function(req , res){ var ID = req.params.id ; //converter... var dl = new Downloader(); var i = 0; var videoTitle; // Declare the variable outside of the function dl.getMP3({videoId:req.params.id},functio ...

What could be causing Recharts to render an unidentified category on the yAxis?

Having trouble understanding why the recharts library is displaying some undefined category on the yAxis. https://i.sstatic.net/wa1vp.png If you'd like to view the code on JSFiddle, here's the link: http://jsfiddle.net/liubko/x1yoboc7/455/ con ...

What is the process for retrieving JSON data from a hosted PHP script using Javascript?

I am currently attempting to retrieve JSON data (stored locally in Xampp) from a PHP script hosted on my web server. The structure of the PHP script is as follows: <?php header('Access-Control-Allow-Origin: *'); header('Access-Control- ...

Hiding the C3 tooltip after engaging with it

I'm currently expanding my knowledge on utilizing C3.js for creating charts, and one aspect I'm focusing on is enhancing the tooltip functionality. Typically, C3 tooltips only appear when you hover over data points as demonstrated in this example ...

AngularJs - Customizable dynamic tabs that automatically adapt

Below is the HTML code snippet: <div> <ul data-ng-repeat ="tab in tabs"> <li data-ng-class="{active: tab.selected == 'true'}" message-key="Tab"> </li> </ul> </div> As shown ...

Error: React.js is unable to access the 'map' property because it is undefined

I have been facing an issue with parsing data from my Web API and displaying it on my react page. The connection is established successfully, and the data is getting parsed (verified by seeing an array of elements in the console). However, when attempting ...

How to Align Content to the Left Inside a Grid List Using Angular Material

Here is the Grid List I am working with: <mat-grid-list cols="3" rowHeight="2:1"> <mat-grid-tile colspan="2">1</mat-grid-tile> <mat-grid-tile>2</mat-grid-tile> </mat-grid-list> The first tile occupies two columns, ...

Ordering an array based on two integer properties using JavaScript

Here is an array of objects I am working with: const items = [ { name: "Different Item", amount: 100, matches: 2 }, { name: "Different Item", amount: 100, matches: 2 }, { name: "An Item", amount: 100, matches: 1 }, { name: "Different Item" ...

Tips on using b-table attributes thClass, tdClass, or class

<template> <b-table striped hover :fields="fields" :items="list" class="table" > </template> <style lang="scss" scoped> .table >>> .bTableThStyle { max-width: 12rem; text-overflow: ellipsis; } < ...

What is the best way to store a small number of files in the state

I have recently implemented Drag and Drop functionality, and now I am facing an issue where I need to save a few files in state. const [uploadedFiles, setUploadedFiles] = useState<any[]>([]); const onDropHandler = async (e: React.DragEvent<HTMLDi ...

Vue.js does not support animation for the Lodash shuffle function

I'm having trouble getting the lodash's shuffle method to animate properly in Vue.js. I followed the code from the documentation, but for some reason, the shuffle occurs instantly instead of smoothly. When I tested the animation with actual item ...

"Clicking on a handler will replace the existing value when the same handler is used for multiple elements in

import React, { useState, useEffect } from "react"; const Swatches = (props) => { const options = props.options; const label = Object.keys(options); const values = Object.values(options); const colors = props.colors; const sizes = p ...