Hoe de plugin het WordPress-datamodel koppelt aan het GraphQL-schema
Zo heeft Gato GraphQL het WordPress-datamodel omgezet naar een bijbehorend GraphQL-schema.
Het WordPress-datamodel
WordPress heeft de volgende entiteiten:
- posts
- pages
- custom posts
- media-elementen
- gebruikers
- gebruikersrollen
- tags
- categorieƫn
- reacties
- blokken
- meta-eigenschappen
- overige (opties, plugins, thema's, enz.)
Deze entiteiten kunnen een hiƫrarchie hebben. Zo zijn post, page en media-elementen allemaal custom post types, en zijn tags en categorieƫn beide taxonomieƫn.
Dit is het WordPress-databasediagram, dat toont hoe gegevens van alle entiteiten worden opgeslagen:

Is de koppeling een exacte kopie van het databasediagram?
Wordt het bovenstaande diagram 1 op 1 gevolgd bij het omzetten van de WordPress-database naar een GraphQL-schema?
Nee, dat is het niet. Het databasediagram is een daadwerkelijke implementatie, maar GraphQL is een interface om gegevens op te halen vanuit de client. Deze twee zijn gerelateerd, maar kunnen verschillen. GraphQL maakt zich niet druk om de database: het denkt niet in SQL-opdrachten en weet niet dat er databasetabellen bestaan die wp_posts en wp_users heten.
We hoeven ons bij het opstellen van het GraphQL-schema voor WordPress dus niet te veel zorgen te maken over het databasediagram. We kunnen zelfs een GraphQL-schema produceren dat een deel van de technische schuld van het WordPress-datamodel oplost.
Het WordPress-datamodel koppelen als GraphQL-schema
Laten we de koppeling maken. Eerst zetten we de oorspronkelijke entiteiten zoveel mogelijk om naar types. Uit de lijst van entiteiten in het WordPress-datamodel maken we de volgende types voor het GraphQL-schema:
PostPageMediaUserUserRolePostTagPostCategoryComment
Vervolgens voegen we alle verwachte velden toe aan elk type. Om het schema weer te geven, kunnen we de SDL (Schema Definition Language) gebruiken. (Dit is alleen voor documentatiedoeleinden; de plugin zelf gebruikt geen SDL om het schema te coderen: alles is PHP-code).
Dit zijn de velden (onder vele andere) voor een Post:
type Post {
id: ID!
title: String
content: String
excerpt: String
date: Date!
}Dit zijn de velden (onder vele andere) voor een User:
type User {
id: ID!
name: String
email: String!
}We maken ook de bijbehorende verbindingen aan: velden die een andere entiteit teruggeven (in plaats van een scalaire waarde zoals een getal of een string). Zo stellen we een post voor die een auteur heeft, en een gebruiker die posts bezit:
type Post {
author: User!
}
type User {
posts: [Post]
}Velden en verbindingen kunnen ook argumenten accepteren. Zo kunnen we Post.dateStr laten opmaken, en User.posts laten filteren, beperken in aantal en sorteren:
type Post {
dateStr(format: String): Date!
}
type User {
posts(
filter: RootPostsFilterInput
pagination: PostPaginationInput
sort: CustomPostSortInput
): [Post!]!
}
input RootPostsFilterInput {
authorIDs: [ID!]
authorSlug: String
categoryIDs: [ID!]
dateQuery: [DateQueryInput!]
excludeAuthorIDs: [ID!]
excludeIDs: [ID!]
hasPassword: Boolean = false
ids: [ID!]
isSticky: Boolean
metaQuery: [CustomPostMetaQueryInput!]
password: String
search: String
status: [FilterCustomPostStatusEnum!]
tagIDs: [ID!]
tagSlugs: [String!]
}
input PostPaginationInput {
limit: Int
offset: Int
}
input CustomPostSortInput {
by: CustomPostOrderByEnum
order: OrderEnum
}
# ...We gaan zo door voor alle entiteiten in het WordPress-datamodel. Als we klaar zijn, komen we uit op het GraphQL-schema voor WordPress, zichtbaar via de Voyager-client (beschikbaar als "Interactive Schema" in het menu van de plugin):

Dit schema heeft overeenkomsten met het WordPress-databasediagram, maar ook een aantal verschillen. Laten we die analyseren.
Operaties zonder entiteit worden gekoppeld als Root-velden
Het WordPress-databasediagram toont hoe gegevens worden opgeslagen, dus er is geen "beginpunt". GraphQL is echter een interface om gegevens op te halen, en daarom moet er een beginfase zijn van waaruit je de query kunt uitvoeren.
Deze beginfase is het Root-type, of nauwkeuriger: de types QueryRoot en MutationRoot (respectievelijk voor queries en mutations).
In deze twee types koppelen we alle operaties die niet afhankelijk zijn van een entiteit, zoals het uitvoeren van get_posts(), get_users() of wp_signon():
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
loginUser(
usernameOrEmail: String!,
password: String!
): User
}Velden hoeven niet dezelfde naam of signatuur te hebben als de operatie die ze vertegenwoordigen. Zo kan het aanroepen van het veld loginUser passender worden geacht dan signOn.
Schema-elementen groeperen
We kunnen verbeteringen toepassen om het schema te vereenvoudigen en nuttiger te maken. Een veld kan bijvoorbeeld al zijn argumenten ontvangen via een input-object, dat over meerdere velden hergebruikt kan worden en het schema gemakkelijker te visualiseren maakt:
type MutationRoot {
loginUser(input: LoginUserByInput!): User
}
input LoginUserByInput {
usernameOrEmail: String!,
password: String!
}Daarnaast kan de respons van een mutation een "payload"-object zijn, dat naast het betreffende object ook de status van de operatie en foutmeldingen kan bevatten:
type MutationRoot {
loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
type RootLoginUserMutationPayload {
errors: [RootLoginUserMutationErrorPayloadUnion!]
status: OperationStatusEnum!
user: User
userID: ID
}
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
| InvalidUserEmailErrorPayload
| InvalidUsernameErrorPayload
| PasswordIsIncorrectErrorPayload
| UserIsLoggedInErrorPayloadAlle mutations vallen onder MutationRoot
Er zijn operaties die wel afhankelijk zijn van een entiteit, zoals wp_update_post(), die op een bepaalde post wordt toegepast. De bijbehorende mutation in het GraphQL-schema moet worden toegevoegd aan het type MutationRoot, omdat dat nu eenmaal zo werkt in GraphQL.
Deze operatie wordt dan als volgt gekoppeld:
type MutationRoot {
updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
input RootUpdatePostFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
id: ID!
status: CustomPostStatusEnum
tags: [String!]
title: String
}Deze plugin ondersteunt ook geneste mutations, aangeboden als opt-in-functie (omdat dit geen standaard GraphQL-gedrag is). Mutations kunnen dan ook onder elk type worden toegevoegd, niet alleen MutationRoot. In dat geval krijgen we:
type Post {
update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
input PostUpdateFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
status: CustomPostStatusEnum
tags: [String!]
title: String
}Let op het verschil tussen de inputs RootUpdatePostFilterInput en PostUpdateFilterInput (dus tussen mutations vanuit de root en geneste mutations): de eerste heeft de verplichte eigenschap id om aan te geven welke post moet worden gewijzigd, maar de tweede niet, omdat die het niet nodig heeft.
Omgaan met custom posts
Er is geen type-overerving in GraphQL. Daarom kunnen we geen type CustomPost aanmaken en verklaren dat Post en Page dit uitbreiden.
GraphQL biedt twee hulpmiddelen om dit gebrek te compenseren: interfaces en union-types.
Voor het eerste maken we een interface CustomPost voor het schema, waarbij we alle velden declareren die van een custom post worden verwacht, en definiëren we de types Post, Page en GenericCustomPost (die alle custom post types vertegenwoordigen die zijn gedefinieerd door een geïnstalleerd thema of plugin) om de interface te implementeren:
interface CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Post implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type Page implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}
type GenericCustomPost implements CustomPost {
title: String
content: String
excerpt: String
date(format: String): Date!
}Voor het tweede maken we een type CustomPostUnion voor het schema dat alle custom post types teruggeeft:
union CustomPostUnion = Post | Page | GenericCustomPostEn we zorgen dat velden dit type teruggeven waar dat passend is:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}Bij het uitvoeren van de query kunnen we velden selecteren op basis van het werkelijke type, zoals Post, of op basis van de interface CustomPost:
{
customPosts {
__typename
...on CustomPost {
id
title
slug
status
}
...on Post {
isSticky
postFormat
}
}
}Zoals te zien is, moeten we in het GraphQL-schema expliciet aangeven wanneer we te maken hebben met posts en wanneer met custom posts, want dat is niet hetzelfde! Deze twee door elkaar gebruiken is technische schuld van WordPress, die de plugin probeert op te lossen waar mogelijk.
Daarom wordt een custom post altijd CustomPost genoemd en niet Post, een veld dat met custom posts te maken heeft wordt altijd customPosts genoemd en niet posts, en een veldargument dat het ID van een custom post ontvangt wordt customPostID genoemd en niet postID (ook al is dat hoe het wordt genoemd in de gekoppelde WordPress-functie).
De verwachting is dan altijd duidelijk:
- Veld
User.customPostskan een lijst van elk custom post type teruggeven, inclusief posts en pagina's, enUser.postsgeeft alleen posts terug - Veld
Root.setFeaturedImageOnCustomPostkan een uitgelichte afbeelding toevoegen aan elk custom post type, daarom heet het nietsetFeaturedImageOnPost
Tags (en categorieƫn) niet samenvoegen onder ƩƩn type
Waarom heet het type PostTag (en hetzelfde geldt voor PostCategory) zo, in plaats van gewoon Tag?
Omdat bij het uitvoeren van deze query (waarbij een product een CPT is), de resultaten van het veld tags voor posts en producten altijd verschillend en niet-overlappend zullen zijn:
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}Tags die aan posts zijn toegevoegd, verschijnen niet bij het ophalen van tags voor producten, en omgekeerd (tenzij een product ook de taxonomie post_tag gebruikt, maar dan kan het ook worden weergegeven met het type PostTag). Dit is geen groot probleem in WordPress, omdat deze items als verschillende rijen uit dezelfde databasetabel kunnen worden beschouwd. Maar het maakt wel uit voor GraphQL, dat sterk getypeerd is.
Het is dan ook een goede ontwerpbeslissing om deze entiteiten apart te houden, elk onder hun eigen type, en tags voor posts terug te geven onder het type PostTag. Als een aangepaste plugin zijn eigen product-CPT implementeert, moet het het type ProductTag gebruiken voor de bijbehorende tags.
Media-elementen een eigen identiteit geven
Media-entiteiten in WordPress zijn custom post types, simpelweg omdat dat vanuit implementatieperspectief handig was. Het GraphQL-schema kan deze technische schuld echter vermijden en media-elementen modelleren als een afzonderlijke entiteit, niet als custom posts.
Dit impliceert de volgende keuzes voor het GraphQL-schema:
- Het type
Mediaimplementeert de interfaceCustomPostniet en maakt geen deel uit van het typeCustomPostUnion - Het type
Mediaheeft veel velden niet die van een custom post type worden verwacht, zoalsexcerpt,dateenstatus. In plaats daarvan heeft het alleen de velden die van een media-element worden verwacht:
type Media {
id: ID!
src: String!
width: Int
height: Int
}Enums identificeren en koppelen
In sommige situaties gebruikt WordPress vaste waarden uit een bepaalde reeks. De status van een post kan bijvoorbeeld alleen "publish", "draft", "pending" of "trash" zijn.
In GraphQL kunnen we deze behandelen als enums (in plaats van strings) en een bijbehorend opsommingstype aanmaken. Volgens de GraphQL-standaard moeten enums in hoofdletters worden geschreven, zoals:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}Dan kan de query echter niet rechtstreeks worden gebruikt om met WordPress te communiceren, omdat get_posts( [ "post_status" => "PUBLISH" ] ) uitvoeren niet werkt.
Daarom houden we als compromis deze enum-waarden in kleine letters:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}Extra types koppelen
Blokken zijn niet direct zichtbaar in het WordPress-databasediagram, omdat ze worden opgeslagen in wp_posts (er is geen tabel wp_blocks), maar ze zijn desondanks een afzonderlijke entiteit.
Daarom kunnen we nog steeds een type Block introduceren om ze te koppelen:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}