Concetti, idee, strategie
Concetti, idee, strategieCome il plugin mappa il modello dati WordPress nello schema GraphQL

Come il plugin mappa il modello dati WordPress nello schema GraphQL

Ecco come Gato GraphQL ha mappato il modello dati WordPress in uno schema GraphQL corrispondente.

Il modello dati WordPress

WordPress possiede le seguenti entità:

  • posts
  • pages
  • custom posts
  • elementi multimediali
  • utenti
  • ruoli utente
  • tag
  • categorie
  • commenti
  • blocchi
  • proprietà meta
  • altri (opzioni, plugin, temi, ecc.)

Queste entità possono avere una gerarchia. Per esempio, post, page ed elementi multimediali sono tutti custom post types, e i tag e le categorie sono entrambi tassonomie.

Ecco il diagramma del database WordPress, che mostra come vengono memorizzati i dati di tutte le entità:

Il diagramma del database WordPress

Il mapping è una replica esatta del diagramma del database?

Quando si mappa il database WordPress in uno schema GraphQL, viene rispettato esattamente lo stesso diagramma mostrato sopra, in rapporto 1 a 1?

No, non è così. Mentre il diagramma del database è un'implementazione reale, GraphQL è un'interfaccia per accedere ai dati dal client. Questi due elementi sono correlati, ma possono essere diversi. A GraphQL non interessa il database: non ragiona in comandi SQL, né sa che esistono tabelle di database chiamate wp_posts e wp_users.

Quindi non dobbiamo preoccuparci troppo del diagramma del database quando creiamo lo schema GraphQL per WordPress. Anzi, possiamo produrre uno schema GraphQL che corregge una parte del debito tecnico del modello dati WordPress.

Mappare il modello dati WordPress come schema GraphQL

Eseguiamo il mapping. Per prima cosa, mappiamo le entità originali come tipi, per quanto possibile. A partire dall'elenco delle entità nel modello dati WordPress, produciamo i seguenti tipi per lo schema GraphQL:

  • Post
  • Page
  • Media
  • User
  • UserRole
  • PostTag
  • PostCategory
  • Comment

Poi, aggiungiamo tutti i campi previsti a ogni tipo. Per rappresentare lo schema, possiamo usare l'SDL, ovvero lo Schema Definition Language. (Questo è solo a scopo di documentazione; il plugin stesso non usa l'SDL per codificare lo schema: è tutto codice PHP).

Questi sono i campi (tra molti altri) per un Post:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
  date: Date!
}

Questi sono i campi (tra molti altri) per un User:

type User {
  id: ID!
  name: String
  email: String!
}

Creiamo anche le connessioni corrispondenti, che sono campi che restituiscono un'altra entità (invece di uno scalare, come un numero o una stringa). Per esempio, rappresentiamo il fatto che un post ha un autore, e che un utente possiede dei post:

type Post {
  author: User!
}
 
type User {
  posts: [Post]
}

I campi e le connessioni possono anche accettare argomenti. Per esempio, consentiamo che Post.dateStr venga formattato, e che User.posts possa filtrare le voci, limitarne il numero e ordinarle:

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
}
 
# ...

Continuiamo così per tutte le entità del modello dati WordPress. Una volta terminato, arriveremo allo schema GraphQL per WordPress, visibile tramite il client Voyager (disponibile come "Interactive Schema" nel menu del plugin):

Lo schema GraphQL per WordPress

Questo schema presenta delle somiglianze con il diagramma del database WordPress, ma anche diverse differenze. Analizziamole.

Le operazioni senza entità sono mappate come campi Root

Il diagramma del database WordPress rappresenta il modo in cui i dati vengono memorizzati, quindi non c'è un "inizio". GraphQL, invece, è un'interfaccia per recuperare dati, perciò deve esserci una fase iniziale da cui eseguire la query.

Questa fase iniziale è il tipo Root, o, per essere più precisi, i tipi QueryRoot e MutationRoot (per gestire rispettivamente le queries e le mutation).

In questi due tipi, mappiamo tutte le operazioni che non dipendono da un'entità, come quando si esegue get_posts(), get_users() o wp_signon():

type QueryRoot {
  posts: [Post]!
  users: [User]!
}
 
type MutationRoot {
  loginUser(
    usernameOrEmail: String!,
    password: String!
  ): User
}

I campi non devono necessariamente avere lo stesso nome o la stessa firma dell'operazione che rappresentano. Per esempio, chiamare il campo loginUser può essere considerato più appropriato di signOn.

Raggruppamento degli elementi dello schema

Possiamo applicare dei miglioramenti per semplificare lo schema e renderlo più utile. Per esempio, un campo può ricevere tutti i suoi argomenti tramite un oggetto input, che può essere riutilizzato in più campi e facilita la visualizzazione dello schema:

type MutationRoot {
  loginUser(input: LoginUserByInput!): User
}
 
input LoginUserByInput {
    usernameOrEmail: String!,
    password: String!
}

Inoltre, la risposta di una mutation può essere un oggetto "payload", che oltre a restituire l'oggetto interessato può anche includere lo stato dell'operazione e i messaggi di errore:

type MutationRoot {
  loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
 
type RootLoginUserMutationPayload {
  errors: [RootLoginUserMutationErrorPayloadUnion!]
  status: OperationStatusEnum!
  user: User
  userID: ID
}
 
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
  | InvalidUserEmailErrorPayload
  | InvalidUsernameErrorPayload
  | PasswordIsIncorrectErrorPayload
  | UserIsLoggedInErrorPayload

Tutte le mutation vanno sotto MutationRoot

Ci sono operazioni che dipendono da un'entità, come wp_update_post(), che viene applicata a un certo post. La mutation corrispondente nello schema GraphQL deve essere aggiunta al tipo MutationRoot, perché è così che funziona GraphQL.

Questa operazione viene quindi mappata così:

type MutationRoot {
  updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
 
input RootUpdatePostFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  id: ID!
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Questo plugin supporta anche le mutation annidate, offerte come funzionalità opt-in (perché non è un comportamento GraphQL standard). In questo modo, le mutation possono essere aggiunte anche sotto qualsiasi tipo, non solo MutationRoot. In questo caso, otteniamo:

type Post {
  update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
 
input PostUpdateFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Si noti la differenza tra gli input RootUpdatePostFilterInput e PostUpdateFilterInput (cioè tra le mutation dalla radice e le mutation annidate): il primo ha la proprietà obbligatoria id per indicare quale post modificare, mentre il secondo non la ha, perché non ne ha bisogno.

Gestione dei custom posts

Non esiste ereditarietà di tipi in GraphQL. Quindi, non possiamo avere un tipo CustomPost e dichiarare che Post e Page lo estendono.

GraphQL offre due risorse per compensare questa mancanza: le interfacce e i tipi union.

Per la prima, creiamo un'interfaccia CustomPost per lo schema, dichiarando tutti i campi previsti da un custom post, e definiamo i tipi Post, Page e GenericCustomPost (per rappresentare tutti i custom post types definiti da qualsiasi tema e plugin installato) affinché implementino l'interfaccia:

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!
}

Per la seconda, creiamo un tipo CustomPostUnion per lo schema che restituisce tutti i custom post types:

union CustomPostUnion = Post | Page | GenericCustomPost

E facciamo in modo che i campi restituiscano questo tipo quando è appropriato:

type QueryRoot {
  customPost(id: ID): CustomPostUnion
  customPosts: [CustomPostUnion]!
}
 
type User {
  customPosts: [CustomPostUnion]
}
 
type Comment {
  customPost: CustomPostUnion!
}

Durante l'esecuzione della query, possiamo selezionare i campi in base al tipo effettivo, come Post, oppure in base all'interfaccia CustomPost:

{  
  customPosts {
    __typename
    ...on CustomPost {
      id
      title
      slug
      status
    }
    ...on Post {
      isSticky
      postFormat
    }
  }
}

Come si può osservare, nello schema GraphQL dobbiamo affermare esplicitamente quando abbiamo a che fare con dei post e quando con dei custom posts, dato che non sono la stessa cosa! Chiamare questi due in modo intercambiabile è un debito tecnico di WordPress, che il plugin cerca di correggere ogni volta che è possibile.

Per questo motivo, un custom post viene sempre chiamato CustomPost e non Post, un campo che gestisce i custom posts viene sempre chiamato customPosts e non posts, e un argomento di campo che riceve l'ID di un custom post viene chiamato customPostID e non postID (anche se è così che viene chiamato nella funzione WordPress mappata).

In questo modo, l'aspettativa è sempre chiara:

  • Il campo User.customPosts può restituire un elenco di qualsiasi custom post, inclusi post e pagine, mentre User.posts restituisce solo i post
  • Il campo Root.setFeaturedImageOnCustomPost può aggiungere un'immagine in evidenza a qualsiasi custom post, ecco perché non si chiama setFeaturedImageOnPost

Non raggruppare i tag (e le categorie) sotto un unico tipo

Perché il tipo PostTag (e lo stesso vale per PostCategory) si chiama così, invece di semplicemente Tag?

Perché, durante l'esecuzione di questa query (dove un prodotto è un CPT), i risultati del campo tags per i post e per i prodotti saranno sempre diversi, senza sovrapposizioni:

query {
  posts {
    tags {
      id
      name
    }
  }
  products {
    tags {
      id
      name
    }
  }
}

I tag aggiunti ai post non compariranno durante il recupero dei tag per i prodotti, e viceversa (a meno che un prodotto non utilizzi anch'esso la tassonomia post_tag, ma in tal caso può essere rappresentato anche con il tipo PostTag). Questo non rappresenta un grosso problema in WordPress, dato che questi elementi possono essere considerati righe diverse della stessa tabella di database. Ma è importante per GraphQL, che è fortemente tipizzato.

Quindi, è una buona decisione di progettazione mantenere queste entità separate, sotto i propri tipi, e fare in modo che i tag per i post vengano restituiti sotto il tipo PostTag e che, se un plugin personalizzato implementa il proprio CPT di prodotto, debba usare il tipo ProductTag per i suoi tag.

Dare agli elementi multimediali una propria identità

Le entità multimediali in WordPress sono custom post types, solo perché era comodo dal punto di vista dell'implementazione. Tuttavia, lo schema GraphQL può evitare questo debito tecnico e modellare gli elementi multimediali come un'entità distinta, e non come custom posts.

Questo comporta le seguenti decisioni per lo schema GraphQL:

  • Il tipo Media non implementa l'interfaccia CustomPost e non farà parte del tipo CustomPostUnion
  • Il tipo Media non ha molti campi previsti da un custom post type, come excerpt, date e status. Invece, ha solo i campi previsti da un elemento multimediale:
type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Identificazione e mapping degli enum

In alcune situazioni, WordPress utilizza valori fissi da un determinato insieme. Per esempio, lo stato di un post può essere solo "publish", "draft", "pending" o "trash".

In GraphQL, possiamo trattarli come enum (invece che come stringhe) e creare un tipo di enumerazione corrispondente. Seguendo lo standard GraphQL, gli enum dovrebbero essere scritti in maiuscolo, in questo modo:

enum CUSTOM_POST_STATUS {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}

Tuttavia, la query non può quindi essere usata direttamente per interagire con WordPress, dato che eseguire get_posts( [ "post_status" => "PUBLISH" ] ) non funziona.

Quindi, come compromesso, manteniamo questi valori enum in minuscolo:

enum CUSTOM_POST_STATUS {
  publish
  draft
  pending
  trash
}

Mapping di tipi aggiuntivi

I blocchi non sono direttamente visibili nel diagramma del database WordPress, dato che vengono memorizzati in wp_posts (non esiste una tabella wp_blocks), ma sono comunque un'entità distinta.

Possiamo quindi introdurre comunque un tipo Block per mapparli:

type Post {
  blocks: [Block]
}
 
type Block {
  type: String!
  attributes: JSONObject
}