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=="
}
}
}
};
CodePudding user response:
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:
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);
CodePudding user response:
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!
