0

A returned data object looks like the following:

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

This is based on the following query:

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

This works and I can use it, but I’m having to create nested interfaces, unfortunately.

Question: Can I either simplify the returned results OR transform them with JavaScript map()?

Ideally, I’d like for the GQL response (or resulting object) to be like:

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

Note: I do not have the ability to update the server-side GraphQL schema. The solution must be client/consumer side.

Thanks!

EDIT

Adding my Angular/TS code that calls and processes the GraphQL...

post.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 { Post } from '../models/post';

export interface PostResponse {
  edges: Post[]
  pageInfo: {
    startCursor: string
    hasPreviousPage: boolean
    hasNextPage: boolean
    endCursor: string
  }
}

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

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

  constructor(private apollo: Apollo) { }

  public getPosts(): Observable<PostResponse> {
   
    return this.apollo.query<GraphQLResponse<'posts', PostResponse>>({
      query: getPostsQuery
    }).pipe(map(resp => resp.data.posts));
  }
}

model/post.ts

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

interface Category {
    category: CategoryNode;
}

interface CategoryEdges{
    edges: Category[];
}

interface PostNode {
    id: string;
    title: string;
    date: string;
    uri: string;
    categories: CategoryEdges;
}

export interface Post {
    article: PostNode;
    cursor: string;
}

As you can see, way too many nested interfaces.

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=="
            }       
          }
        }
      }; 
a11smiles
  • 1,190
  • 1
  • 9
  • 21
  • "*A returned schema looks like*" - that's not the schema, that's an example of a returned object. Can you post the actual schema declaration, please? – Bergi Jan 07 '22 at 23:56
  • What graphql library do you use on the client side? This looks a bit like a Relay connection. If the library is aware of that, it might be able to return a simplified structure. – Bergi Jan 07 '22 at 23:58
  • My apologies, I missed a level of curly braces. I’ve updated the returned object. I’m using Wordpress GraphQL plugin server-side and Apollo in my Angular app. – a11smiles Jan 08 '22 at 00:02
  • @Bergi I've updated the post for clarity, but the schema is quite large, so I've just included my query. – a11smiles Jan 08 '22 at 03:16
  • Yeah, please don't post the entire schema, but only the types that you're using. For examples, I'm quite certain that you can simplify the query to `posts { nodes { id title } }` but I can't tell without the actual schema. – Bergi Jan 08 '22 at 03:19
  • Also yes you can [transform or otherwise process the structure](https://stackoverflow.com/q/11922383/1048572) using `map`. Have you tried it? Can you show us the code for your approach? It should be quite simple. – Bergi Jan 08 '22 at 03:21
  • @Bergi I can do the query you suggested, but I still get `{ "data": { "posts": { "nodes": [ { "id": "1", "title": "Foo" } ] } } }`. Is there no way to "elevate" the _nodes_ array to be the posts, instead of posts having a single _nodes_ property with an array? – a11smiles Jan 08 '22 at 03:23
  • @Bergi I've updated the post with my Angular service and models. Additionally, I've provided a sample output that I use for mocking in my unit test. Again, the current solution works. I just feel like there are too many nested interfaces. – a11smiles Jan 08 '22 at 03:34

2 Answers2

0

Without being able to introspect the GQL schema, it's difficult to advise you on how to modify your query to get the shape that you want (if it's possible), but without modifying your query, you can transform the response value into the shape that you want like this:

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);
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
0

I ended up using nested map()'ing to transform the GraphQL response to a "cleaner" object.

Below is my final code, if anyone has the same question/issue.

NOTE: In the code below I'm using "articles" instead of "posts," but it's the same concept.

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

Below you will notice that I'm transforming the server response within the service and testing the response from the service to ensure it was transformed as expected.

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();

  });
});

Thanks everyone for your input and assistance!

a11smiles
  • 1,190
  • 1
  • 9
  • 21