Skip to main content

GraphQL

Why GraphQL?

Prior to implementing GraphQL, clients had to make multiple requests and manage complex logic to determine which items should be displayed and in what order. For instance, the Newsfeed would make separate requests for the latest posts, boosts, in-feed notices, top post highlights, and channel recommendations. It was the responsibility of the app to handle and organize the received data for display.

With GraphQL, a single query now provides all this information and can be controlled from the backend. This approach simplifies the client-side logic and reduces the number of network requests required to fetch the data. The backend can handle the complexity of aggregating the requested information and optimize the query execution for efficient data retrieval.

Pagination

Minds makes use of Relay (Cursor Connection) style pagination. All lists, regardless of if they currently require pagination should use the Connection pattern.

Connection

A Connection is another name for a list. A connection should always contain the edges and pageInfo types.

Edges

An Edge should contain a Node and a cursor, but it may also return other relevant information about the relationship that may not be appropriate at the Node level, for example; the reason an Activity is in the list (mutual followers, recent activity) or campaign data for a Boost.

Node

A Node is a wrapper around an Entity. For example, the ActivityNode is a GraphQL Type representation of the Activity Entity.

A Node should contain an ID field, that can be a string of any format.

A Node can be any type, even a Connection, such as PublisherRecsConnection or FeedHighlightsConnection.

Legacy field

The legacy field is temporary solution to allow feeds to be migrated to GraphQL without remodelling all of our data types. It is a stringified JSON object.

PageInfo

The PageInfo type has four fields, hasNextPage, hasPreviousPage, startCursor (optional), endCursor (optional).

If no paging is available, or required, the PageInfo should still be returned, but may have false/null values.

Eg:

$pageInfo = new Types\PageInfo(
hasPreviousPage: false,
hasNextPage: false,
startCursor: null,
endCursor: null,
);
...
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": null,
"endCursor": null
}
...

Cursor

A Cursor should be a base64 encoded string. There is no required structure to the decoded cursor, it may be an integer, string, or an array. For example, in the 'latest' and 'groups' newsfeed algorithms (see here), we provide an array of values that the OpenSearch query has given us, and will accept in future calls to assist with the pagination.

Example

Let's take a look at our most popular Connection, the NewsfeedConnection / Newsfeed Query.

Query

query FetchNewsfeed($algorithm: String!, $limit: Int!, $cursor: String) {
newsfeed(algorithm: $algorithm, first: $limit, after: $cursor) {

edges {
__typename
cursor
node {
__typename
id
}
}

pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}

Response

{
"data": {
"newsfeed": {
"edges": [
{
"__typename": "FeedNoticeEdge",
"cursor": "WzE2ODQ4NTM0MjIwMDAsIjE1MDc3NTc2MDQ4MTY4MTgxODQiXQ==",
"node": {
"__typename": "FeedNoticeNode",
"id": "feed-notice-supermind-pending"
}
},
{
"__typename": "ActivityEdge",
"cursor": "WzE2ODQ4NTM0MjIwMDAsIjE1MDc3NTc2MDQ4MTY4MTgxODQiXQ==",
"node": {
"__typename": "ActivityNode",
"id": "activity-1507757604816818184"
}
},
{
"__typename": "ActivityEdge",
"cursor": "WzE2ODQ4NTMzNjUwMDAsIjE1MDc3NTczNjQ0ODczOTMyODIiXQ==",
"node": {
"__typename": "ActivityNode",
"id": "activity-1507757364487393282"
}
},
{
"__typename": "ActivityEdge",
"cursor": "WzE2ODQ4NDk5MDcwMDAsIjE1MDc3NDI4NjE3NDU5ODM1MDIiXQ==",
"node": {
"__typename": "ActivityNode",
"id": "activity-1507742861745983502"
}
},
{
"__typename": "PublisherRecsEdge",
"cursor": "MA==",
"node": {
"__typename": "PublisherRecsConnection",
"id": "publisher-recs"
}
},
{
"__typename": "ActivityEdge",
"cursor": "WzE2ODQ4NDk4MDMwMDAsIjE1MDc3NDI0MjQ1OTQ2NDkwOTUiXQ==",
"node": {
"__typename": "ActivityNode",
"id": "activity-1507742424594649095"
}
},
{
"__typename": "ActivityEdge",
"cursor": "WzE2ODQ3NzA2MzQwMDAsIjE1MDc0MTAzNjY3MTAxNTczMzEiXQ==",
"node": {
"__typename": "ActivityNode",
"id": "activity-1507410366710157331"
}
},
{
"__typename": "ActivityEdge",
"cursor": "WzE2ODQ3NzA2MDYwMDAsIjE1MDc0MTAyNDgxOTU5MDM0OTIiXQ==",
"node": {
"__typename": "ActivityNode",
"id": "activity-1507410248195903492"
}
},
{
"__typename": "FeedHighlightsEdge",
"cursor": "WzAsIjEyOTE3ODM0OTM2NDIwOTY2NTUiXQ==",
"node": {
"__typename": "FeedHighlightsConnection",
"id": "feed-highlights"
}
},
{
"__typename": "FeedNoticeEdge",
"cursor": "WzE2ODQ3Njk5MDAwMDAsIjE1MDc0MDcyODc0MzY3MDk5MDAiXQ==",
"node": {
"__typename": "FeedNoticeNode",
"id": "feed-notice-build-your-algorithm"
}
},
{
"__typename": "ActivityEdge",
"cursor": "WzE2ODQ3Njk5MDAwMDAsIjE1MDc0MDcyODc0MzY3MDk5MDAiXQ==",
"node": {
"__typename": "ActivityNode",
"id": "activity-1507407287436709900"
}
},
{
"__typename": "ActivityEdge",
"cursor": "WzE2ODQ3Njk4MjgwMDAsIjE1MDc0MDY5ODI5Njc5ODgyNDEiXQ==",
"node": {
"__typename": "ActivityNode",
"id": "activity-1507406982967988241"
}
}
],
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": true,
"startCursor": "WzE2ODU3MDk4MTMwMDAsIjE1MTEzNDk1NjU2OTE3OTM0MTQiXQ==",
"endCursor": "WzE2ODQ3NjkxMTIwMDAsIjE1MDc0MDM5ODMzNjk1Mzk1OTkiXQ=="
}
}
}
}

Backend: Getting Started

A module should declare its types in a GraphQLMappings class, that should be intitalized from its respective Modules->onInit() function. For example:

<?php
namespace Minds\Core\Feeds\GraphQL;

use Minds\Core\GraphQL\AbstractGraphQLMappings;
use TheCodingMachine\GraphQLite\Mappers\StaticClassListTypeMapperFactory;

class GraphQLMappings extends AbstractGraphQLMappings
{
public function register(): void
{
$this->schemaFactory->addControllerNamespace('Minds\Core\Feeds\GraphQL\Controllers');
$this->schemaFactory->addTypeMapperFactory(new StaticClassListTypeMapperFactory([
Types\NewsfeedConnection::class,
Types\ActivityEdge::class,
Types\ActivityNode::class,
Types\UserEdge::class,
Types\UserNode::class,
Types\FeedHighlightsEdge::class,
Types\FeedHighlightsConnection::class,
Types\PublisherRecsEdge::class,
Types\PublisherRecsConnection::class,
]));
}
}

Controllers

Controllers hold Query types. Queries are the equivalent of a GET endpoint in a REST API.

Frontend: Getting Started

.graphql files

All queries should be placed in their own .graphql files. As we use both Strapi and our own engine backend, the .graphql suffixes should be prefixed with either .engine, or .strapi. See fetch-newsfeed.engine.graphql for an example.

Codegen

Codegen auto generates Typescript typings from the GraphQL schema. There are different commands used to generate these from either the Minds engine or Strapi:

# Engine
npm run graphql:codegen:compile:engine

# Strapi
npm run graphql:codegen:compile:strapi

These commands will output your types in src/generated.engine.ts and src/generated.strapi.ts respectively, alongside services that wrap the apollo client to enable simpler retrieval of query results (see below):

Fetching a query

Following executing the codegen command above, your query will now be available to be injected in your component.

You can then initiate a GraphQL call like so:

const fetchFeed = this.fetchNewsfeed.watch(
{
algorithm: this.algorithm,
limit: PAGE_SIZE,
},
{
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
notifyOnNetworkStatusChange: true,
errorPolicy: 'all',
}
);

const feedObservable = fetchFeed.valueChanges; // This is an RxJS observable.

You must use the fetch function instead of watch if you only need to request the data once.

Paginating

To implement pagination, you should extend the typePolicies, to reference your query, and indicate you wish to use the relayStylePagination.

export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
newsfeed: relayStylePagination(['limit', 'algorithm']),
},
},
},
});

More data can be requested by calling the fetchMore() function. Example here.

Apollo (the GraphQL client we use), will return all the cached data back on every subsequent call. This can make navigation sluggish, so clients may wish to implement slicing logic. See the Newsfeed implementation for reference.

Performance

Ensure any *ngFor list includes a trackyBy function, and you use the Node ID, or another identifier, as the reference.