Closed callmephilip closed 4 months ago
Hello @callmephilip, This topic idea sounds interesting, you can go ahead. :) If you need help on anything, feel free to reach out to me here on or Discord.
@hanakhelifa sounds good! i'll get initial draft ready and share it here to review
@hanakhelifa here's the first version of the article. looking forward to your feedback
In this post we will modify the GraphQL schema that Strapi generates to include a new custom type and extend the existing type to include a new field. The whole project is on Github; the main extension code is here
schema
, resolver
INNER JOINS
and aggregations
Let's say we are building an API for Politician Trust Meter, giving us stats on how trustworthy a given politician is based on his/her previous track record. We got our hands on LIAR dataset hosted on Hugging Face. Here's the dataset summary:
LIAR is a dataset for fake news detection with 12.8K human labeled short statements from politifact.com's API, and each statement is evaluated by a politifact.com editor for its truthfulness. The distribution of labels in the LIAR dataset is relatively well-balanced: except for 1,050 pants-fire cases, the instances for all other labels range from 2,063 to 2,638. In each case, the labeler provides a lengthy analysis report to ground each judgment.
After some cosmetic merging and processing, we load the dataset into Strapi. We end up with the following database schema:
We can now run queries to get a specific politician alongside some additional data. We can also find statements associated with a given politician. Front end folks come to us with the following sketch of the interface
How can we tweak our current schema to include stats for every politician based on their statements? What if we could extend Politician
object to include dynamically calculated stats without changing the underlying data structure and keep things nice and normalized. Our goal is to produce a graphql schema that would look smth like this:
type PoliticianHonestyStat {
label: ENUM_STATEMENT_LABEL!
count: Int!
}
type Politician {
name: String!
job: String
state: String
party: String
createdAt: DateTime
updatedAt: DateTime
stats: [PoliticianHonestyStat!]
}
Turns out we can do it in Strapi! But before we jump in, let's look under the hood and see how Strapi handles GraphQL.
Once you add graphql
plugin to your Strapi project, you ✨ automagically ✨ get all your content APIs exposed via /grapqhl
endpoint - you get types, queries, mutations. Strapi does all the heavy lifting behind the scenes using GraphQL Nexus.
In GraphQL Nexus, you define your GraphQL schema in code using js/ts
as opposed to using GraphQL SDL. Here's an example
import { objectType } from "nexus";
export const Post = objectType({
name: "Post", // <- Name of your type
definition(t) {
t.int("id"); // <- Field named `id` of type `Int`
t.string("title"); // <- Field named `title` of type `String`
t.string("body"); // <- Field named `body` of type `String`
t.boolean("published"); // <- Field named `published` of type `Boolean`
},
});
As you can see, writing these schemas is a pretty tedious task, so it's great that Strapi takes care of it. However, graphql
plugin does expose additional APIs allowing us to tap into the underlying Nexus machine to customize application GraphQL schema. Let's see how!
Let's get back to our app. Here are the tasks we need to go through to roll a new schema:
PoliticianHonestyStat
that includes aggregated statsPolitician
object to include a list of stats of type PoliticianHonestyStat
But first, how do we get hold of Nexus inside Strapi? We do so using extension
service exposed by graphql
plugin:
const extensionService = strapi.plugin("graphql").service("extension");
extensionService.use(({ nexus, strapi }: { nexus: Nexus; strapi: Strapi }) => {
return {
types: [
// we will return new types
// and extend existing types here
],
};
});
We will begin by declaring a new type called PoliticianHonestyStat
containing 2 fields: label
and count
. Notice how label
is typed as ENUM_STATEMENT_LABEL
, which was generated by Strapi for Enum field belonging to Statement
content type. Our definition looks as follows:
nexus.objectType({
name: "PoliticianHonestyStat",
definition(t) {
t.nonNull.field("label", {
type: "ENUM_STATEMENT_LABEL",
});
t.nonNull.int("count");
},
}),
Next up, extending Politician
object type to include a list of our newly crafted PoliticianHonestyStat
:
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// XX: shortcut!!!
// let's leave this empty for now,
// we'll get back here in a minute
return [];
},
});
},
}),
If we now inspect GraphQL schema generated by our app (you can use your local GraphiQL instance at http://localhost:1337/graphql), we will be able to locate 2 following definitions:
type PoliticianHonestyStat {
label: ENUM_STATEMENT_LABEL!
count: Int!
}
type Politician {
name: String!
job: String
state: String
party: String
createdAt: DateTime
updatedAt: DateTime
stats: [PoliticianHonestyStat!]
}
We can even go ahead and run a query to test things out:
query {
politicians {
data {
id
attributes {
name
party
stats {
label
count
}
}
}
}
}
Which gives back smth like this:
{
"data": {
"politicians": {
"data": [
{
"id": "1",
"attributes": {
"name": "rick-perry",
"party": "republican",
"stats": []
}
},
{
"id": "2",
"attributes": {
"name": "katrina-shankland",
"party": "democrat",
"stats": []
}
},
{
"id": "3",
"attributes": {
"name": "donald-trump",
"party": "republican",
"stats": []
}
}
/* more stuff here*/
]
}
}
}
So this kinda works but the stats are just not there. Remember that little shortcut from above? Well, it's time to fix it:
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// XX: shortcut!!!
return [];
},
});
},
}),
There are at least a couple of ways to handle this. We could use Strapi's entity service API to pull stats for a given politician and then do some math adding things up, OR we could leverage Strapi's raw database handle and write some sweet sweet SQL to count things for us:
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// parent points to the instance of the Politician entity
const { id } = parent;
return strapi.db.connection.raw(`
SELECT COUNT(statements.id) as "count", statements.label
FROM politicians
INNER JOIN statements_politician_links ON statements_politician_links.politician_id = politicians.id
INNER JOIN statements ON statements.id = statements_politician_links.statement_id
WHERE politicians.id = ${id}
GROUP BY statements_politician_links.politician_id, statements.label
`);
},
});
},
});
This little bit of SQL exposes some other Strapi's internals around database schema. Without getting into too many details that are beyond the scope of this article, let's take a quick look at what is happening.
You can look at the database for your local app by opening
data.db
inside.tmp
directory in the root of the project using your favorite SQLite client
There are 3 tables of interest for us here: politicians
, statements
and statements_politician_links
. The first 2 are pretty straightforward, as they map directly to the collections we have defined in our app. The 3rd table, statements_politician_links
, connects Politicians and Statements collection together, it has 2 fields (not counting its primary key ID); one of them points to the politician
table while the other one points to statement
telling us which statement belongs to which politician.
Given this schema and some INNER JOIN and GROUP BY kung fu, we are able to pull all statements for a given politician, group them by label and then count how many statements per label we have.
Let's head to index.ts
in /src
and put the whole thing together:
import type { Strapi } from "@strapi/types";
import type * as Nexus from "nexus";
import { nonNull } from "nexus";
type Nexus = typeof Nexus;
export default {
/**
* An asynchronous register function that runs before
* your application is initialized.
*
* This gives you an opportunity to extend code.
*/
register({ strapi }) {
const extensionService = strapi.plugin("graphql").service("extension");
extensionService.use(
({ nexus, strapi }: { nexus: any; strapi: Strapi }) => {
return {
types: [
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
const { id } = parent;
return strapi.db.connection
.raw(`SELECT COUNT(statements.id) as "count", statements.label
FROM politicians
INNER JOIN statements_politician_links ON statements_politician_links.politician_id = politicians.id
INNER JOIN statements ON statements.id = statements_politician_links.statement_id
WHERE politicians.id = ${id}
GROUP BY statements_politician_links.politician_id, statements.label`);
},
});
},
}),
nexus.objectType({
name: "PoliticianHonestyStat",
definition(t) {
t.nonNull.field("label", {
type: "ENUM_STATEMENT_LABEL",
});
t.nonNull.int("count");
},
}),
],
};
}
);
},
async bootstrap({ strapi }) {
// some stuff here
},
};
Let's go ahead and test this using America's 45th president as an example:
query {
politicians(filters: { name: { eq: "donald-trump" } }) {
data {
id
attributes {
name
party
stats {
label
count
}
}
}
}
}
Which gives:
{
"data": {
"politicians": {
"data": [
{
"id": "3",
"attributes": {
"name": "donald-trump",
"party": "republican",
"stats": [
{
"label": "barely_true",
"count": 63
},
{
"label": "half_true",
"count": 51
},
{
"label": "lie",
"count": 117
},
{
"label": "mostly_true",
"count": 37
},
{
"label": "pants_fire",
"count": 61
},
{
"label": "truth",
"count": 14
}
]
}
}
]
}
}
}
In this post, we presented a use case for extending existing GraphQL schema that you might encounter in your Strapi projects. We discovered how Strapi integrates GraphQL in its stack and how we can work with it to modify data layout to suit our use case. You can find the codebase for the project on Github and take it for a spin locally.
Hi @callmephilip, I absolutely loved your article: the copy is quite proximate to the reader, with a funny tone, while providing solid technical knowledge. I especially liked the fact that you took time to doodle some skecthes–that's awesome!
To add some suggestions, I copied the text in a google doc right here: https://docs.google.com/document/d/1_8Cp_tt9tvLomO57CAv2relT5lELVFSdJK86ri8j-4Y/edit?usp=sharing (you should have access to it).
I mainly changed the introduction, to set the context a bit more using the brief you sent previously–the rest was awesome.
Let me know what you think of the suggestions, and once it's done I'd be happy to have you as a guest author on Strapi's blog and have it published once we're done
@hanakhelifa thank you for your kind words. gonna review your comments later today and get the new version ready 🙌
When managing a growing Strapi project, you'll likely encounter the need to customize its GraphQL API for better performance and functionality. This article delves into the intricacies of Strapi's GraphQL schema and provides practical guidance on enhancing it.
By the end of this read, you'll have a clearer picture of how GraphQL functions within Strapi and the steps to tailor it for specific project needs. We'll explore adding a new custom type and expanding an existing one with additional fields in Strapi's GraphQL schema. For hands-on insights, the entire project and the core extension code are available on GitHub.
schema
and resolver
.Imagine we're tasked with creating an API for a 'Politician Trust Meter'. This tool aims to quantify a politician's trustworthiness based on their historical statements. Our primary resource is the LIAR dataset from Hugging Face, a collection designed for detecting fake news.
The LIAR dataset comprises 12.8K human-annotated statements from politifact.com. Each statement has been assessed for truthfulness by a politifact.com editor. The distribution of labels in the LIAR dataset is relatively well-balanced: except for 1,050 pants-fire cases, the instances for all other labels range from 2,063 to 2,638.
After some cosmetic merging and processing, we load the dataset into Strapi. We end up with the following database schema:
We can now run queries to get a specific politician alongside some additional data. We can also find statements associated with a given politician. Front end folks come to us with the following sketch of the interface
How can we tweak our current schema to include stats for every politician based on their statements? What if we could extend Politician
object to include dynamically calculated stats without changing the underlying data structure and keep things nice and normalized. Our goal is to produce a graphql schema that would look smth like this:
type PoliticianHonestyStat {
label: ENUM_STATEMENT_LABEL!
count: Int!
}
type Politician {
name: String!
job: String
state: String
party: String
createdAt: DateTime
updatedAt: DateTime
stats: [PoliticianHonestyStat!]
}
Turns out we can do it in Strapi! But before we jump in, let's look under the hood and see how Strapi handles GraphQL.
Once you add graphql
plugin to your Strapi project, you ✨ automagically ✨ get all your content APIs exposed via /grapqhl
endpoint - you get types, queries, mutations. Strapi does all the heavy lifting behind the scenes using GraphQL Nexus.
In GraphQL Nexus, you define your GraphQL schema in code using js/ts
as opposed to using GraphQL SDL. Here's an example
import { objectType } from "nexus";
export const Post = objectType({
name: "Post", // <- Name of your type
definition(t) {
t.int("id"); // <- Field named `id` of type `Int`
t.string("title"); // <- Field named `title` of type `String`
t.string("body"); // <- Field named `body` of type `String`
t.boolean("published"); // <- Field named `published` of type `Boolean`
},
});
As you can see, writing these schemas is a pretty tedious task. Fortunately, Strapi simplifies this process significantly. However, the real power lies in the additional APIs provided by the GraphQL plugin. These APIs grant access to the underlying Nexus framework, enabling deep customizations of the application's GraphQL schema. Let's see how!
Let's get back to our app. Here are the tasks we need to go through to roll the new schema:
PoliticianHonestyStat
: This new type will encapsulate aggregated stats for each politician.Politician
object: We'll add a field to the Politician
type that lists PoliticianHonestyStat
entries.But first, how do we get hold of Nexus inside Strapi? We do so using extension
service exposed by graphql
plugin:
const extensionService = strapi.plugin("graphql").service("extension");
extensionService.use(({ nexus, strapi }: { nexus: Nexus; strapi: Strapi }) => {
return {
types: [
// we will return new types
// and extend existing types here
],
};
});
We will begin by declaring a new type called PoliticianHonestyStat
containing 2 fields: label
and count
. Notice how label
is typed as ENUM_STATEMENT_LABEL
, which was generated by Strapi for Enum field belonging to Statement
content type. Our definition looks as follows:
nexus.objectType({
name: "PoliticianHonestyStat",
definition(t) {
t.nonNull.field("label", {
type: "ENUM_STATEMENT_LABEL",
});
t.nonNull.int("count");
},
}),
Next up, extending Politician
object type to include a list of our newly crafted PoliticianHonestyStat
:
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// XX: shortcut!!!
// let's leave this empty for now,
// we'll get back here in a minute
return [];
},
});
},
}),
If we now inspect GraphQL schema generated by our app (you can use your local GraphiQL instance at http://localhost:1337/graphql), we will be able to locate 2 following definitions:
type PoliticianHonestyStat {
label: ENUM_STATEMENT_LABEL!
count: Int!
}
type Politician {
name: String!
job: String
state: String
party: String
createdAt: DateTime
updatedAt: DateTime
stats: [PoliticianHonestyStat!]
}
We can even go ahead and run a query to test things out:
query {
politicians {
data {
id
attributes {
name
party
stats {
label
count
}
}
}
}
}
Which gives back smth like this:
{
"data": {
"politicians": {
"data": [
{
"id": "1",
"attributes": {
"name": "rick-perry",
"party": "republican",
"stats": []
}
},
{
"id": "2",
"attributes": {
"name": "katrina-shankland",
"party": "democrat",
"stats": []
}
},
{
"id": "3",
"attributes": {
"name": "donald-trump",
"party": "republican",
"stats": []
}
}
/* more stuff here*/
]
}
}
}
So this kinda works but the stats are just not there. Remember that little shortcut from above? Well, it's time to fix it:
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// XX: shortcut!!!
return [];
},
});
},
}),
There are at least a couple of ways to handle this. We could use Strapi's entity service API to pull stats for a given politician and then do some math adding things up, OR we could leverage Strapi's raw database handle and write some sweet sweet SQL to count things for us:
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// parent points to the instance of the Politician entity
const { id } = parent;
return strapi.db.connection.raw(`
SELECT COUNT(statements.id) as "count", statements.label
FROM politicians
INNER JOIN statements_politician_links ON statements_politician_links.politician_id = politicians.id
INNER JOIN statements ON statements.id = statements_politician_links.statement_id
WHERE politicians.id = ${id}
GROUP BY statements_politician_links.politician_id, statements.label
`);
},
});
},
});
This little bit of SQL exposes some other Strapi's internals around database schema. Without getting into too many details that are beyond the scope of this article, let's take a quick look at what is happening.
You can look at the database for your local app by opening
data.db
inside.tmp
directory in the root of the project using your favorite SQLite client
There are 3 tables of interest for us here: politicians
, statements
and statements_politician_links
. The first 2 are pretty straightforward, as they map directly to the collections we have defined in our app. The 3rd table, statements_politician_links
, connects Politicians and Statements collection together, it has 2 fields (not counting its primary key ID); one of them points to the politician
table while the other one points to statement
telling us which statement belongs to which politician.
Given this schema and some INNER JOIN and GROUP BY kung fu, we are able to pull all statements for a given politician, group them by label and then count how many statements per label we have.
Let's head to index.ts
in /src
and put the whole thing together:
import type { Strapi } from "@strapi/types";
import type * as Nexus from "nexus";
import { nonNull } from "nexus";
type Nexus = typeof Nexus;
export default {
/**
* An asynchronous register function that runs before
* your application is initialized.
*
* This gives you an opportunity to extend code.
*/
register({ strapi }) {
const extensionService = strapi.plugin("graphql").service("extension");
extensionService.use(
({ nexus, strapi }: { nexus: any; strapi: Strapi }) => {
return {
types: [
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
const { id } = parent;
return strapi.db.connection
.raw(`SELECT COUNT(statements.id) as "count", statements.label
FROM politicians
INNER JOIN statements_politician_links ON statements_politician_links.politician_id = politicians.id
INNER JOIN statements ON statements.id = statements_politician_links.statement_id
WHERE politicians.id = ${id}
GROUP BY statements_politician_links.politician_id, statements.label`);
},
});
},
}),
nexus.objectType({
name: "PoliticianHonestyStat",
definition(t) {
t.nonNull.field("label", {
type: "ENUM_STATEMENT_LABEL",
});
t.nonNull.int("count");
},
}),
],
};
}
);
},
async bootstrap({ strapi }) {
// some stuff here
},
};
Let's go ahead and test this using America's 45th president as an example:
query {
politicians(filters: { name: { eq: "donald-trump" } }) {
data {
id
attributes {
name
party
stats {
label
count
}
}
}
}
}
Which gives:
{
"data": {
"politicians": {
"data": [
{
"id": "3",
"attributes": {
"name": "donald-trump",
"party": "republican",
"stats": [
{
"label": "barely_true",
"count": 63
},
{
"label": "half_true",
"count": 51
},
{
"label": "lie",
"count": 117
},
{
"label": "mostly_true",
"count": 37
},
{
"label": "pants_fire",
"count": 61
},
{
"label": "truth",
"count": 14
}
]
}
}
]
}
}
}
In this post, we presented a use case for extending existing GraphQL schema that you might encounter in your Strapi projects. We discovered how Strapi integrates GraphQL in its stack and how we can work with it to modify data layout to suit our use case. You can find the codebase for the project on Github and take it for a spin locally.
Looks perfect to me, thank you! I'd say it's good to be published on Strapi's blog 🎉
@hanakhelifa cool! what do I need to do next then?
This has been published here https://strapi.io/blog/graph-ql-api-customizations-explained-fine-tuning-your-strapi-experience thanks @callmephilip
As your Strapi based project grows in complexity and scale, you will find yourself faced with the challenge of customising its GraphQL API to be performant and eloquent. In this article, we go beyond standard GraphQL schema implementation and take a sneak peak under the hood. After reading this article, you will gain understanding of how GraphQL is integrated into Strapi and how you can customise it to fit your use cases.
Content Brief:
My content is
What do you all think?
❤️ I love it! 🚀 I can help you!