Créer son propre plugin source GatsbyJS pour consommer une API REST — 4 : Activity reporting

Mélanie Paque
Publicis Sapient France
6 min readOct 18, 2023

Cet article est la suite de Plugin Gatsby / API REST — 3 : création, modification et suppression de noeuds

Avec les articles précédents, nous avons conçu un plugin source GatsbyJS.

Si le plugin fonctionne et remplit son rôle de mise à disposition de source exploitable par Gatsby, il lui manque un petit quelque chose : les retours d’informations dans le terminal et le suivi des tâches en cours.

Pour cela, nous allons mettre en place un activity reporting.

Reporter Gatsby

Jusqu’à maintenant, pour afficher de l’information dans notre terminal, nous avons utilisé des console.log.

Il existe une solution plus adaptée, avec l’usage du reporter Gatsby.

Il se faisait discret mais il était déjà là, dans le projet, utilisé dans le catch :

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

try {

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

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

Le reporter permet d’afficher différents types de message : verbose, warning, panic, etc.

Il propose également un timer reporter que nous allons utiliser pour encadrer les différentes tâches réalisées afin de mieux structurer nos retours d’informations en console.

Rapport d’activité

Nous allons encadrer les différentes tâches réalisées en utilisant un activityTimer via le reporter.

Commençons par notre tâche de fetch :

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

try {
const fetchActivity = reporter.activityTimer(`${APP_NAME}: Fetch data`, {
parentSpan,
})
fetchActivity.start()
await fetchCountries()
fetchActivity.end()

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


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

Nous voyons apparaître dans les logs le nouveau message suivant :

Nous poursuivons dans cette lancée et faisons de même pour la tâche de traitement des données.

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

try {
const fetchActivity = reporter.activityTimer(`${APP_NAME}: Fetch data`, {
parentSpan,
})
fetchActivity.start()
await fetchCountries()
fetchActivity.end()

const processingActivity = reporter.activityTimer(
`${APP_NAME}: Process data`,
{
parentSpan,
}
)
processingActivity.start()
await Promise.all(Object.values(executions).map(processData))
processingActivity.end()

}

De même pour la création et la suppression de noeuds.

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

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

//...

const deletionActivity = reporter.activityTimer(
`${APP_NAME}: Deleting ${nodeType} nodes `,
{
parentSpan,
}
)
deletionActivity.start()
for (const node of execution.deleted) {
deleteNode(node)
}
deletionActivity.end()

const creationActivity = reporter.activityTimer(
`${APP_NAME}: Creating ${nodeType} nodes`,
{
parentSpan,
}
)
creationActivity.start()
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),
},
})
}
creationActivity.end()

resolve()
})

Nous aurons ainsi, lors du build, un rapport d’activité avec le suivi de nos tâches réalisées.

Reporter info

Nous allons modifier le message à l’initialisation du plugin.

Pour rendre notre code d’avantage générique, nous allons faire quelques modifications :

Pour le nom du plugin, nous utilisons le name déclaré dans le package.json et l’exportons via le fichier config.js

./plugins/gatsby-source-countries/package.json

{
"name": "gatsby-source-countries",
"version": "1.0.0",

}

./plugins/gatsby-source-countries/src/config.js

const packageJson = require('../package.json')
const PLUGIN_NAME = packageJson.name

//...

module.exports = {
PLUGIN_NAME,
APP_NAME,
NODES_KEY,
NODE_TYPES,
CACHE_KEY
}

Nous utilisons ensuite le reporter pour afficher le compte des éléments added, deleted, updated et cached.

Pour cela, nous allons créer une fonction logExecution.

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

const logExecution = (execution) => {
const { added, updated, deleted, cached, nodeType } = execution
reporter.info(`${APP_NAME}: ${added.length} new ${nodeType}`)
reporter.info(`${APP_NAME}: ${updated.length} updated ${nodeType}`)
reporter.info(
`${APP_NAME}: ${deleted.length - updated.length} deleted ${nodeType}`
)
reporter.info(`${APP_NAME}: ${cached.length} cached ${nodeType}`)

En plus du compte des noeuds, nous souhaitons également afficher un message indiquant que les données ont été mise en cache à une clé de cache définie.

Nous complétons notre objet executions.country en ajoutant cacheKey :

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

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

et ajoutons le message de mise en cache dans la fonction logExecution

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

const logExecution = (execution) => {
const { added, updated, deleted, cached, cacheKey, nodeType } = execution
//...
reporter.info(`${APP_NAME}: data cached on cache key ${cacheKey} `)
}

Nous appelons logExecution en fin d’exécution :

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

try {
const fetchActivity = reporter.activityTimer(`${APP_NAME}: Fetch data`, {
parentSpan,
})
fetchActivity.start()
await fetchCountries()
fetchActivity.end()

const processingActivity = reporter.activityTimer(
`${APP_NAME}: Process data`,
{
parentSpan,
}
)
processingActivity.start()
await Promise.all(Object.values(executions).map(processData))
processingActivity.end()

Object.values(executions).forEach(logExecution)

}

Nous en profitons pour améliorer également un peu le message d’erreur appelé en cas d’échec. Cette fois nous n’utilisons pas info mais panic. Cette méthode affiche une message d’erreur et quitte coupe le process en cours. Plus besoin alors d’utiliser process.exit().

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

}catch (e) {
reporter.panic({
id: '10000',
context: {
sourceMessage: e.message,
},
})
}

et mettons aussi à jour onPreInit.

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

const {APP_NAME, NODES_KEY, NODE_TYPES, CACHE_KEY, PLUGIN_NAME} = require("./src/config")

//...

exports.onPreInit = ({reporter}
) => reporter.info(`Loaded ${PLUGIN_NAME}`)

Pour obtenir finalement au build l’affichage suivant dans le terminal :

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, PLUGIN_NAME} = require("./src/config")

exports.onPreInit = ({reporter}) => reporter.info(`Loaded ${PLUGIN_NAME}`)

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,
parentSpan
}) => {
const { createNode,deleteNode,touchNode } = actions

const {COUNTRY} = NODES_KEY

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

const logExecution = (execution) => {
const { added, updated, deleted, cached, cacheKey, nodeType } = execution
reporter.info(`${APP_NAME}: ${added.length} new ${nodeType}`)
reporter.info(`${APP_NAME}: ${updated.length} updated ${nodeType}`)
reporter.info(
`${APP_NAME}: ${deleted.length - updated.length} deleted ${nodeType}`
)
reporter.info(`${APP_NAME}: ${cached.length} cached ${nodeType}`)
reporter.info(`${APP_NAME}: data cached on cache key ${cacheKey} `)
}

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',
}})

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, nodeType} = execution

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

case STATUS.NOT_MODIFIED: {
execution.cached = [...nodes]
break
}

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

const deletionActivity = reporter.activityTimer(
`${APP_NAME}: Deleting ${nodeType} nodes `,
{
parentSpan,
}
)
deletionActivity.start()
for (const node of execution.deleted) {
deleteNode(node)
}
deletionActivity.end()

const creationActivity = reporter.activityTimer(
`${APP_NAME}: Creating ${nodeType} nodes`,
{
parentSpan,
}
)
creationActivity.start()
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),
},
})
}
creationActivity.end()

resolve()
})

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

try {
const fetchActivity = reporter.activityTimer(`${APP_NAME}: Fetch data`, {
parentSpan,
})
fetchActivity.start()
await fetchCountries()
fetchActivity.end()

const processingActivity = reporter.activityTimer(
`${APP_NAME}: Process data`,
{
parentSpan,
}
)
processingActivity.start()
await Promise.all(Object.values(executions).map(processData))
processingActivity.end()

Object.values(executions).forEach(logExecution)

}catch (e) {
reporter.panic({
id: '10000',
context: {
sourceMessage: e.message,
},
})
}
}

Conclusion

Nous avons maintenant lors de notre build un reporting complet de suivi des tâches et du traitement de nos nœuds.

Nous clôturons ainsi cette série d’articles sur la création d’un plugin source GatsbyJS, finalisant notre plugin source Gatsby nous permettant d’ajouter et enrichir nos sources de données.

S’il existe déjà bien des plugins source, que vous pourrez trouver dans la bibliothèque de plugins GatsbyJS, vous pouvez désormais créer le vôtre indépendamment et selon vos besoins.

Il y a encore bien d’autres fonctionnalités que nous n’avons pas exploré ici, telles que la création de lien entre les noeuds, l’implémentation de resolver, la customisation des options du plugin etc.

Les curieux seront ravis, vous avez encore bien des sujets à explorer pour aller plus loin et renforcer votre plugin!

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

--

--