Créer son propre plugin source GatsbyJS pour consommer une API REST — 3 : Ajout, modification et suppression de noeuds

Mélanie Paque
Publicis Sapient France
11 min readJun 13, 2023

Cet article est la suite de l’article: Créer son propre plugin source GatsbyJS pour consommer une API REST — 2 : Cache et noeuds persistants

Nous avons vu dans les précédents articles de cette série comment réaliser un plugin source GatsbyJS, l’utilisation du cache et des nœuds persistants.

Dans cet article nous optimiserons davantage notre utilisation du cache et des nœuds persistants en affinant notre traitement des données (ajout, modification et suppression).

Traitement des données personnalisé

Utilisation d’un jeu de données en local pour la phase de développement

Les données de l’API REST Countries n’étant pas régulièrement mises à jour, nous n’aurons pas de différence entre la valeur en cache et la réponse de l’API lorsque nous développerons notre nouveau traitement des données.

Afin de mieux pouvoir éprouver notre code et notre avancée, nous allons donc utiliser un fichier local, contenant un mock JSON, dans lequel nous pourrons ajouter, modifier et supprimer les données selon nos besoins.

Nous créons pour cela un dossier ./data, puis un fichier countries.json. Ce fichier contiendra un extrait de la réponse renvoyée par l’API REST Countries, soit un tableau avec les données de 3 pays.

Le fichier JSON est disponible en ligne ici.

Nous utilisons ensuite ce jeu de données à la place de la réponse API. Nous passons en commentaire :

./plugins/gatsby-source-countries/gatsby-node.js

const fetch = require('node-fetch')
const CACHE_KEY = 'restcountries-last-response'
const countriesJSON = require("./data/countries.json")

//...

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {

//...

const fetchCountries = async () => {
/*
const response = await fetch(`https://restcountries.com/v3.1/all`, {
method: 'GET',
headers : {
'Content-Type': 'application/json',
},
})
const countries = await response.json()
*/

const countries = countriesJSON
execution.items = countries

//...
}

//...

Pour partir sur une base de cache vide, supprimez ce dernier via la commande :

npm run clean

Lorsque vous lancez votre projet en local, seul trois pays s’affichent désormais :

Maintenant que nous avons notre source de données en local, modulable à souhait, nous pouvons commencer notre développement.

Statut des données

Nous allons pouvoir nous atteler aux différents cas d’ajout, modification ou suppression de nœuds. Afin de savoir dans lequel de ces cas nous entrerons, nous comparons la valeur en cache et la valeur courante de la réponse API. Nous en définirons un statut.

Ce statut aura une valeur définie selon les 3 cas suivants :

  • non initialisé : il n’y a pas de données en cache
  • non modifié : il n’y a pas de changement entre la valeur en cache et la réponse API courante
  • modifié : la valeur en cache et la réponse API courante ne sont pas identique

./plugins/gatsby-source-countries/gatsby-node.js

//import...

exports.onPreInit = () => console.log("Loaded gatsby-source-countries")

const STATUS = {
NOT_INITIALIZED: 'not-initialized',
NOT_MODIFIED: 'not-modified',
MODIFIED: 'modified',
}

Nous définissons également une fonction getStatus par laquelle nous effectuerons la comparaison des valeurs et retournerons le statut correspondant :

./plugins/gatsby-source-countries/gatsby-node.js

const STATUS = {
NOT_INITIALIZED: 'not-initialized',
NOT_MODIFIED: 'not-modified',
MODIFIED: 'modified',
}

const getStatus = (cached, current) => {
if (!cached) {
return STATUS.NOT_INITIALIZED
}
if (cached === current) {
return STATUS.NOT_MODIFIED
}
return STATUS.MODIFIED
}

Nous renommons ensuite hasChanged de execution par changeStatus.

Nous ajoutons également les tableaux suivants :

  • added : il comprendra les items pour lesquels devra être créé les nouveaux noeuds correspondant
  • updated : il comprendra les items modifiés depuis le dernier build et dont les noeuds ne sont plus à jours
  • deleted : il comprendra les noeuds à supprimer, soit les noeuds dont les items correspondant n’existe plus depuis le dernier build. De plus la modification d’un node étant réalisé par la suppression de l’ancien noeuds et la création d’un nouveau noeud à jour, ce tableau comprendra également les noeuds dont l’item correspondant à changé ( Nous y reviendrons ).
  • cached: il comprendra les noeuds qui n’ont pas changé depuis le dernier build

./plugins/gatsby-source-countries/gatsby-node.js

const execution = {
nodeType: 'RestcountriesCountry',
changeStatus : false,
nodes: [],
items: [],
added: [],
updated: [],
deleted: [],
cached: [],
}

Dans la fonction fetchCountries, nous remplaçons notre condition if(!responseCached) par un appel à la fonction getStatus. Nous récupérons le statut et attribuons cette valeur à changeStatus.

./plugins/gatsby-source-countries/gatsby-node.js

 const fetchCountries = async () => {
/*
const response = await fetch(`https://restcountries.com/v3.1/all`, {
method: 'GET',
headers : {
'Content-Type': 'application/json',
},
})
const countries = await response.json()
*/

const countries = countriesJSON
execution.items = countries

const responseCached = await cache.get(CACHE_KEY)

const status = getStatus(responseCached, JSON.stringify(countries))
execution.changeStatus = status

cache.set(CACHE_KEY, JSON.stringify(countries))

}

Puis dans notre fonction processData, nous déclarons un switch case dans lequel nous effectuerons un traitement différent selon la valeur de changeStatus :

./plugins/gatsby-source-countries/gatsby-node.js

const processData = (execution) =>
new Promise((resolve) => {
const { changeStatus, items } = execution

switch (changeStatus) {
case STATUS.NOT_INITIALIZED: {
//
break
}

case STATUS.NOT_MODIFIED: {
//
break
}

case STATUS.MODIFIED: {
//
break
}
}

//...
})

Traitement au cas par cas

Nous allons effectuer notre traitement selon les cas de figure possibles.

Commençons par le cas où il n’y pas de données en cache : NOT_INITIALIZED.

Cas 1 : non initialisé

Dans le cas NOT_INITIALIZED, tous les nœuds doivent être créés.

Nous ajoutons donc tous les items à execution.added.

Pour tous les items dans added, nous créons ensuite des noeuds.

./plugins/gatsby-source-countries/gatsby-node.js

const processData = (execution) =>
new Promise((resolve) => {
const { changeStatus, items, nodes } = execution

switch (changeStatus) {
case STATUS.NOT_INITIALIZED: {
console.log(`case : ${STATUS.NOT_INITIALIZED}`)
execution.added = [...items]
break
}

case STATUS.NOT_MODIFIED: {
//
break
}

case STATUS.MODIFIED: {
//
break
}
}

console.log('nbr added : ', execution.added.length )

const itemsToCreate = execution.added
for (const item of itemsToCreate) {
const nodeContent = JSON.stringify(item)
createNode({
...item,
id: createNodeId(`restcountries-country-${item.name.common}`),
parent: null,
children: [],
internal: {
type: 'RestcountriesCountry',
mediaType: `application/json`,
content: nodeContent,
contentDigest: createContentDigest(item),
},
})
}
resolve()
})

Pour tester, nous vidons notre cache avec npm run clean, puis relançons le projet en local.

En console, nous pouvons voir que le tableau added comprend bien 3 items :

Nous vérifions également avec le serveur local GraphqQL que nos noeuds ont bien été créés.

Cas 2 : non modifié

Passons au cas NOT_MODIFIED.

Dans ce cas là, tous les nœuds sont conservés. Nous ajoutons donc à execution.cached l’ensemble des noeuds persistants.

./plugins/gatsby-source-countries/gatsby-node.js

const processData = (execution) =>
new Promise((resolve) => {
const { changeStatus, items, nodes } = execution

switch (changeStatus) {
case STATUS.NOT_INITIALIZED: {
console.log(`case : ${STATUS.NOT_INITIALIZED}`)
execution.added = [...items]
break
}

case STATUS.NOT_MODIFIED: {
console.log(`case : ${STATUS.NOT_MODIFIED}`)
execution.cached = [...nodes]
break
}

case STATUS.MODIFIED: {
//
break
}
}

console.log('nbr added : ', execution.added.length )
console.log('nbr cached : ', execution.cached.length )
//...

Puis nous relançons un build successif.

Nous vérifions que les nœuds ne sont pas recréés alors qu’ils ont déjà été générés lors du build précédent : le tableau added doit être vide et les trois nœuds doivent désormais être dans le tableau cached.

Dans le terminal, nous voyons ainsi s’afficher :

Cas 3 : modifié

Voyons maintenant le cas MODIFIED

Pour celui-ci nous allons avoir plusieurs cas possible à traiter :

  • des données ont été supprimées
  • de nouvelles données ont été ajoutées
  • des données ont été modifiées

Commençons par la suppression.

Suppression de données

Nous allons chercher à savoir si des données ont été supprimées.

Pour cela nous cherchons parmi les nœuds persistants si chacun à son item correspondant par rapport aux données récupérées via l’API.

Pour se faire, nous allons utiliser le champ id.

Nous indiquons dans execution un idName, soit id, nom du champ par lequel nous ferons la correspondance.

./plugins/gatsby-source-countries/gatsby-node.js

const execution = {
nodeType: 'RestcountriesCountry',
changeStatus : false,
idName : 'id',
nodes: [],
items: [],
added: [],
updated: [],
deleted: [],
cached: [],
}

Pour la recherche de correspondance permettant de déterminer si des nœuds ont été supprimés, nous commençons par parcourir nos nœuds persistants.

Si parmi les items, aucun item ayant le même id n’existe, alors cela signifie que le country a été supprimé et nous l’ajoutons au tableau execution.deleted.

./plugins/gatsby-source-countries/gatsby-node.js

const processData = (execution) =>
new Promise((resolve) => {
const { changeStatus, items, nodes , idName} = execution

switch (changeStatus) {
case STATUS.NOT_INITIALIZED: {
console.log(`case : ${STATUS.NOT_INITIALIZED}`)
execution.added = [...items]
break

}

case STATUS.NOT_MODIFIED: {
console.log(`case : ${STATUS.NOT_MODIFIED}`)
execution.cached = [...nodes]
break
}

case STATUS.MODIFIED: {
console.log(`case : ${STATUS.MODIFIED}`)
nodes.forEach((node) => {
if (!items.find((item) => item[idName] === node[idName])) {
execution.deleted.push(node)
}
})
break
}
}

console.log('nbr added : ', execution.added.length )
console.log('nbr cached : ', execution.cached.length )
console.log('nbr deleted : ', execution.deleted.length )

Pour supprimer les noeuds contenus dans le tableau deleted, nous itérons dans ce dernier et appelons le helper deleteNode sur chacun des nodes.

./plugins/gatsby-source-countries/gatsby-node.js

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {
const { createNode,deleteNode,touchNode } = actions

//...

const processData = (execution) =>
new Promise((resolve) => {
const { changeStatus, items, nodes , idName} = execution

switch (changeStatus) {
case STATUS.NOT_INITIALIZED: {
console.log(`case : ${STATUS.NOT_INITIALIZED}`)
execution.added = [...items]
break
}

case STATUS.NOT_MODIFIED: {
console.log(`case : ${STATUS.NOT_MODIFIED}`)
execution.cached = [...nodes]
break
}


case STATUS.MODIFIED: {
console.log(`case : ${STATUS.MODIFIED}`)
nodes.forEach((node) => {
if (!items.find((item) => item[idName] === node[idName])) {
execution.deleted.push(node)
}
})
break
}
}

console.log('nbr added : ', execution.added.length )
console.log('nbr cached : ', execution.cached.length )
console.log('nbr deleted : ', execution.deleted.length )

for (const node of execution.deleted) {
deleteNode(node)
}

const itemsToCreate = execution.added
//...

Pour tester la suppression, nous allons supprimer dans notre fichier .data/countries.json un country du tableau et relancer le projet.

Nous observons que le nœud a bien été supprimé.

Nous pouvons passer au cas suivant.

Nouvelles données

Nous cherchons à savoir s’il y a de nouvelles données, de nouveaux objets country.

En nous basant toujours sur l’idName, nous allons faire une nouvelle recherche de correspondance où cette fois, si aucun node n’a le même idName qu’un des items, alors celui-ci est nouveau. Nous l’ajoutons alors au tableau added.

./plugins/gatsby-source-countries/gatsby-node.js

case STATUS.MODIFIED: {
console.log(`case : ${STATUS.MODIFIED}`)
nodes.forEach((node) => {
if (!items.find((item) => item[idName] === node[idName])) {
execution.deleted.push(node)
}
})

items.forEach((item) => {
const existingNode = nodes.find(
(node) => node[idName] === item[idName]
)

if (existingNode) {

} else {
execution.added.push(item)
}
})

break
}

Données modifiées

Enfin, nous nous intéressons au cas où les données ont été modifié.

Pour définir si la donnée a été modifiée, nous allons comparer la valeur du internal content du nœud persistant avec l’item correspondant.

Si l’internal content du nœud et l’item ne sont pas identiques, alors on peut en déduire qu’il y a eu une modification.

Il faut savoir que la modification d’un noeud revient à supprimer l’ancien node pour recréer un noeud à jour.

Nous ajoutons donc le noeud persistant qui n’est plus à jour au tableau de noeuds deleted afin qu’il soit supprimé, et l’item modifié au tableau updated.

Nous ajoutons ensuite à itemsToCreate les items contenus dans le tableau updated afin que ces noeuds modifiés soient recréés.

./plugins/gatsby-source-countries/gatsby-node.js

case STATUS.MODIFIED: {
console.log(`case : ${STATUS.MODIFIED}`)
nodes.forEach((node) => {
if (!items.find((item) => item[idName] === node[idName])) {
execution.deleted.push(node)
}
})

items.forEach((item) => {
const existingNode = nodes.find(
(node) => node[idName] === item[idName]
)

if (existingNode) {
if (existingNode.internal.content !== JSON.stringify(item)) {
execution.deleted.push(existingNode)
execution.updated.push(item)
} else {
execution.cached.push(existingNode)
}
} else {
execution.added.push(item)
}
})

break
}


console.log('nbr added : ', execution.added.length )
console.log('nbr cached : ', execution.cached.length )
console.log('nbr deleted : ', (execution.deleted.length - execution.updated.length) )
console.log('nbr updated : ', execution.updated.length )

//...

const itemsToCreate = [...execution.added, ...execution.updated]

Petite remarque concernant les console.log décomptant les suppressions: la modification d’un noeud impliquant sa suppression et “re-création”, nous affichons désormais le nombre d’éléments deleted auquel nous soustrayons le nombre d’éléments modifiés.

Pour tester notre code nous modifions un objet country ( la population de Chypre par exemple ) dans notre fichier .data/countries.json et relançons le projet en local.

Nous avions avant :

Nous avons désormais :

et dans notre terminal s’affiche :

Et…c’est terminé !

Ci-dessous le code complet de gatsby-node.js :

./plugins/gatsby-source-countries/gatsby-node.js

const {API_URL, getAllCountries} = require('./src/api')
const {APP_NAME, NODES_KEY, NODE_TYPES, CACHE_KEY} = require("./src/config")
const countriesJSON = require("./data/countries.json")

exports.onPreInit = () => console.log("Loaded gatsby-source-countries")

const STATUS = {
NOT_INITIALIZED: 'not-initialized',
NOT_MODIFIED: 'not-modified',
MODIFIED: 'modified',
}

const getStatus = (cached, current) => {
if (!cached) {
return STATUS.NOT_INITIALIZED
}
if (cached && cached === current) {
return STATUS.NOT_MODIFIED
}
return STATUS.MODIFIED
}

exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
reporter,
getNodesByType,
cache
}) => {
const { createNode,deleteNode,touchNode } = actions

const {COUNTRY} = NODES_KEY

const executions = {
[COUNTRY]: {
nodeType: NODE_TYPES[COUNTRY],
changeStatus : false,
idName : 'cca3',
nodes: [],
items: [],
added: [],
updated: [],
deleted: [],
cached: [],
}
}


const prepareNodes = (execution) =>
new Promise((resolve) => {
const { nodeType } = execution
execution.nodes = getNodesByType(nodeType)
for (const node of execution.nodes) {
touchNode(node)
}
resolve()
})

const fetchCountries = async () => {
/*
const countries = await getAllCountries({
apiUrl: API_URL,
headers : {
'Content-Type': 'application/json',
}})
*/

const countries = countriesJSON
executions.country.items = countries


const responseCached = await cache.get(CACHE_KEY.ALL)

const status = getStatus(responseCached, JSON.stringify(countries))
executions.country.changeStatus = status

cache.set(CACHE_KEY.ALL, JSON.stringify(countries))
}

const processData = (execution) =>
new Promise((resolve) => {
const { changeStatus, items, nodes , idName} = execution

switch (changeStatus) {
case STATUS.NOT_INITIALIZED: {
console.log(`case : ${STATUS.NOT_INITIALIZED}`)
execution.added = [...items]
break
}

case STATUS.NOT_MODIFIED: {
console.log(`case : ${STATUS.NOT_MODIFIED}`)
execution.cached = [...nodes]
break
}

case STATUS.MODIFIED: {
console.log(`case : ${STATUS.MODIFIED}`)
nodes.forEach((node) => {
if (!items.find((item) => item[idName] === node[idName])) {
execution.deleted.push(node)
}
})

items.forEach((item) => {
const existingNode = nodes.find(
(node) => node[idName] === item[idName]
)

if (existingNode) {
if (existingNode.internal.content !== JSON.stringify(item)) {
execution.deleted.push(existingNode)
execution.updated.push(item)
} else {
execution.cached.push(existingNode)
}
} else {
execution.added.push(item)
}
})

break
}
}

console.log('nbr added : ', execution.added.length )
console.log('nbr cached : ', execution.cached.length )
console.log('nbr deleted : ', (execution.deleted.length - execution.updated.length) )
console.log('nbr updated : ', execution.updated.length )

for (const node of execution.deleted) {
deleteNode(node)
}

const itemsToCreate = [...execution.added, ...execution.updated]

for (const item of itemsToCreate) {
const nodeContent = JSON.stringify(item)
const id = item.cca3

createNode({
...item,
id: createNodeId(`${APP_NAME}-${NODES_KEY.COUNTRY}-${id}`),
parent: null,
children: [],
internal: {
type: NODE_TYPES[NODES_KEY.COUNTRY],
mediaType: `application/json`,
content: nodeContent,
contentDigest: createContentDigest(item),
},
})
}
resolve()
})


await Promise.all(
Object.values(executions).map((execution) => prepareNodes(execution))
)

try {

await fetchCountries()
await Promise.all(Object.values(executions).map(processData))


}catch (e) {
console.error(e)
reporter.error(e.message)
process.exit()
}
}

Conclusion

Nous avons désormais affiné notre traitement des données.

Nous ne réalisons sur nos nœuds que des actions adaptées et uniquement lorsque celles-ci sont nécessaires.

Notre travail concernant les nœuds de notre plugin source est terminé.

Si nous en avons fini avec la gestion de noeuds, il nous reste encore une amélioration à faire sur notre plugin : les retours d’informations dans le terminal (il y a plus adapté que les console.log !) et le suivi des tâches en cours.

C’est ce que nous verrons dans le prochain article avec la mise en place d’activity reports.

Vous pouvez retrouvez l’ensemble du code du tutorial sur github ici.

--

--