Travailler avec des données inattendues dans JavaScript





L'un des principaux problèmes avec les langages à typage dynamique est que vous ne pouvez pas toujours garantir un flux de données correct car vous ne pouvez pas forcer un paramètre ou une variable à être défini sur une valeur autre que null, par exemple. Dans de tels cas, nous avons tendance à utiliser du code simple:



function foo (mustExist) {
  if (!mustExist) throw new Error('Parameter cannot be null')
  return ...
}


Le problème avec cette approche est la pollution du code, car vous devez tester des variables partout et il n'y a aucun moyen de garantir que tous les développeurs exécuteront réellement ce test toujours, en particulier dans les situations où une variable ou un paramètre ne peut pas être nul. Souvent, nous ne savons même pas qu'un tel paramètre peut avoir la valeur indéfinie ou nulle - cela se produit souvent lorsque différents spécialistes travaillent sur les parties client et serveur, c'est-à-dire dans la grande majorité des cas.



Pour optimiser un peu ce scénario, j'ai commencé à chercher comment et avec quelles stratégies il est préférable de minimiser le facteur de surprise. C'est là que je suis tombé sur un excellent article d'Eric Elliott.... Le but de ce travail n'est pas de réfuter complètement son article, mais d'ajouter des informations intéressantes que j'ai pu découvrir au fil du temps grâce à mon expérience dans le domaine du développement JavaScript.



Avant de commencer, j'aimerais passer en revue certains des points abordés dans cet article et exprimer mon opinion en tant que développeur de composants serveur, car un autre article est plus orienté client.



Comment tout a commencé



Le problème du traitement des données peut être dû à plusieurs facteurs. La raison principale, bien sûr, est l'entrée de l'utilisateur. Cependant, il existe d'autres sources de données mal formées en plus de celles mentionnées dans un autre article:



  • Enregistrements de base de données
  • Fonctions qui renvoient implicitement des données nulles
  • API externes


Dans tous les cas considérés, différentes solutions s'appliqueront, et plus tard nous analyserons chacune d'elles en détail, en nous rappelant qu'aucune n'est une panacée. La plupart des problèmes sont causés par une erreur humaine: dans de nombreux cas, les langues sont prêtes à travailler avec des données nulles ou non définies (nulles ou non définies), cependant, lors du processus de transformation de ces données, la capacité de les traiter peut être perdue.



Données saisies par l'utilisateur



Dans ce cas, nous avons très peu d'opportunités. Si le problème réside dans l'entrée de l'utilisateur, il peut être résolu avec la soi-disant hydratation (en d'autres termes, nous devons prendre l'entrée brute que l'utilisateur nous envoie (par exemple, dans le cadre d'une charge utile d'API) et la transformer en quelque chose avec lequel nous pouvons travailler sans erreur).



Côté serveur, lors de l'utilisation d'un serveur Web comme Express, nous pouvons effectuer toutes les opérations avec une entrée utilisateur côté client en utilisant des outils standards comme le schéma JSON  ou Joi .



Un exemple de ce qui peut être fait avec Express ou AJV est donné ci-dessous:



const Ajv = require('ajv')
const Express = require('express')
const bodyParser = require('body-parser')
 
const app = Express()
const ajv = new Ajv()
 
app.use(bodyParser.json())
 
app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      name: { type: 'string' },
      password: { type: 'string' },
      email: { type: 'string', format: 'email' }
    },
    additionalProperties: false
    required: ['name', 'password', 'email']
  }
 
  const valid = ajv.validate(schema, req.body)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})
 
app.listen(3000)


Regardez: nous vérifions la partie principale de l'itinéraire. Par défaut, c'est l'objet que nous obtenons du package body-parser dans le cadre de la charge utile. Dans ce cas, nous le transmettons via le schéma JSON , il sera donc validé si l'une de ces propriétés est d'un type ou d'un format différent (dans le cas du courrier électronique).



Important! Notez que nous retournons un HTTP 422 pour un objet non traité . De nombreuses personnes interprètent une erreur de requête, telle qu'un corps ou une chaîne de requête non valide, comme une erreur 400  Requête non valide - c'est en partie vrai, mais dans ce cas, le problème ne résidait pas dans la demande elle-même, mais dans les données que l'utilisateur a envoyées avec elle. La réponse optimale à l'utilisateur serait donc l'erreur 422: cela signifie que la requête est correcte, mais qu'elle ne peut pas être traitée car son contenu n'est pas au format attendu.



Une autre option (en plus d'utiliser AJV) est d'utiliser la bibliothèque que j'ai créée avec Roz . Nous l'avons appelé Expresso , et c'est un ensemble de bibliothèques qui facilitent un peu le développement d'API utilisant Express. Un de ces outils est  @ expresso / validator , qui fait essentiellement ce que nous avons démontré ci-dessus, mais peut être remis en tant que middleware.



Paramètres supplémentaires avec des valeurs par défaut



En plus de ce que nous avons vérifié précédemment, nous avons trouvé qu'il était possible de transmettre une valeur nulle à notre application au cas où elle ne serait pas envoyée dans un champ facultatif. Imaginons, par exemple, que nous ayons une route de pagination qui prend deux paramètres, page et taille, comme chaînes de requête. Cependant, ils sont facultatifs et doivent être définis par défaut s'ils ne sont pas reçus.



Idéalement, notre contrôleur devrait avoir une fonction qui fait quelque chose comme ceci:



function searchSomething (filter, page = 1, size = 10) {
  // ...
}


Remarque. Tout comme avec l'erreur 422 que nous avons renvoyée en réponse aux demandes de pagination, il est important de renvoyer le bon code d'erreur, 206 Contenu incomplet , chaque fois que nous répondons à une demande pour laquelle la quantité de données renvoyée fait partie d'un tout, nous retournons 206. Lorsque l'utilisateur a atteint la dernière page et qu'il n'y a plus de données, nous pouvons renvoyer un code de 200, et lorsque l'utilisateur essaie de trouver une page en dehors de la plage totale de pages, nous renvoyons le code 204 No content .



Cela résoudrait le problème lorsque nous obtenons deux valeurs vides, mais c'est un aspect très controversé de JavaScript en général. Les paramètres facultatifs prennent une valeur par défaut uniquement si la valeur est vide, mais cette règle ne fonctionne pas pour la valeur null, donc si nous faisons ce qui suit:



function foo (a = 10) {
  console.log(a)
}
 
foo(undefined) // 10
foo(20) // 20
foo(null) // null


et nous avons besoin que les informations soient traitées comme nulles, nous ne pouvons pas nous fier uniquement aux paramètres optionnels pour cela. Par conséquent, dans de tels cas, nous avons deux façons:



1. Utilisez les instructions If dans le contrôleur



function searchSomething (filter, page = 1, size = 10) {
  if (!page) page = 1
  if (!size) size = 10
  // ...
}


Cela n'a pas l'air très bien et est plutôt gênant.



2. Utilisez les schémas JSON  directement sur la route



Encore une fois, nous pouvons utiliser AJV ou @ expresso / validator pour valider ces données:



app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      page: { type: 'number', default: 1 },
      size: { type: 'number', default: 10 },
    },
    additionalProperties: false
  }
 
<a href=""></a>  const valid = ajv.validate(schema, req.params)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})


Utilisation de valeurs nulles et non définies



Je ne suis personnellement pas satisfait de l'idée d'utiliser à la fois null et undefined en JavaScript pour prouver que la valeur est vide, pour plusieurs raisons. En plus des difficultés à amener ces concepts au niveau abstrait, il ne faut pas oublier les paramètres optionnels. Si vous avez encore des doutes sur ces concepts, laissez-moi vous donner un excellent exemple de la pratique:







maintenant que nous comprenons les définitions, nous pouvons dire qu'en 2020, il y aura deux fonctions majeures dans JavaScript: l'opérateur de fusion nul et chaînage optionnel . Je n'entrerai pas dans les détails maintenant, puisque j'ai  déjà écrit un article à ce sujet. (c'est en portugais), mais notez que ces deux innovations vont grandement simplifier notre tâche, puisque nous pouvons nous concentrer sur ces deux concepts, null et indéfini avec l'opérateur approprié (??), au lieu d'utiliser des négatifs logiques comme! obj qui sont un terrain fertile pour les erreurs.



Fonctions qui retournent implicitement null



Ce problème est beaucoup plus difficile à résoudre en raison de sa nature implicite. Certaines fonctions traitent les données en supposant qu'elles seront toujours fournies, mais dans certains cas, ce n'est pas le cas. Prenons un exemple standard:



function foo (num) {
  return 23*num
}


Si num est nul, le résultat de cette fonction sera 0, ce qui n'était pas attendu. Dans de tels cas, nous n'avons d'autre choix que de tester le code. Il existe deux types de tests qui peuvent être effectués. La première consiste à utiliser une instruction if simple:



function foo (num) {
  if (!num) throw new Error('Error')
  return 23*num
}


La deuxième façon consiste à utiliser la monade Either , qui est discutée en détail dans l'article que j'ai mentionné. C'est un excellent moyen de gérer des données ambiguës, c'est-à-dire des données qui peuvent ou non être nulles. En effet, JavaScript a déjà une fonction intégrée qui prend en charge deux flux d'actions, Promise:



function exists (value) {
  return x != null ? Promise.resolve(value) : Promise.reject(`Invalid value: ${value}`)
}
 
async function foo (num) {
  return exists(num).then(v => 23 * v)
}


Voici comment vous pouvez déléguer l'instruction catch depuis existe à la fonction qui a appelé foo:



function init (n) {
  foo(n)
    .then(console.log)
    .catch(console.error)
}
 
init(12) // 276
init(null) // Invalid value: null


API externes et enregistrements de base de données



C'est un cas très courant, en particulier lorsqu'il existe des systèmes développés à partir de bases de données créées ou remplies plus tôt. Par exemple, un nouveau produit qui utilise la même base de données que son prédécesseur à succès, intégrant ainsi les utilisateurs de différents systèmes, et ainsi de suite.



Le gros problème avec cela n'est pas le fait que la base de données est inconnue - en fait, c'est la raison, puisque nous ne savons pas ce qui a été fait au niveau de la base de données, et nous ne pouvons pas confirmer si nous recevrons des données avec une valeur nulle ou indéfinie ou non. ... Nous ne pouvons que parler d'une documentation de mauvaise qualité lorsque la base de données n'est pas correctement documentée et que nous sommes confrontés au même problème qu'auparavant.



Il n'y a pratiquement rien que nous puissions faire ici, et personnellement je préfère vérifier l'état des données pour m'assurer que je peux travailler avec elles. Cependant, vous ne pouvez pas valider toutes les données, car la plupart des objets renvoyés peuvent simplement être trop volumineux. Par conséquent, avant d'effectuer toute opération, il est recommandé de vérifier les données impliquées dans le fonctionnement de la fonction, telles qu'une carte ou un filtre, pour s'assurer qu'elles ne sont pas définies ou non.



Générer des erreurs



Il est recommandé d'utiliser des fonctions d'assertion  pour les bases de données et les API externes. Essentiellement, ces fonctions renvoient des données, le cas échéant, et une erreur est générée dans le cas contraire. Le cas d'utilisation le plus courant pour ce type de fonction est lorsque nous avons une API, par exemple pour rechercher un type de données spécifique par identifiant, le bien connu findById:



async function findById (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await entityRepository.findById(id)
  if (!result) throw new EntityNotFoundError(id)
  return result
}


Remplacez Entity par le nom de votre entité, par exemple UserNotFoundError.



C'est bien, car nous pouvons avoir une fonction au sein du même contrôleur pour trouver des utilisateurs par ID et une autre fonction qui utilise cet utilisateur pour trouver d'autres données, par exemple, les profils de cet utilisateur dans une autre collection de bases de données. Lors de l'appel de la fonction de recherche de profil, nous utilisons une assertion pour nous assurer que l'utilisateur existe réellement dans notre base de données. Sinon, la fonction ne sera même pas exécutée et vous pourrez rechercher l'erreur directement sur l'itinéraire:



async function findUser (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await userRepository.findById(id)
  if (!result) throw new UserNotFoundError(id)
  return result
}
 
async function findUserProfiles (userId) {
  const user = await findUser(userId)
 
  const profile = await profileRepository.findById(user.profileId)
  if (!profile) throw new ProfileNotFoundError(user.profileId)
  return profile
}


Notez que nous ne ferons pas d'appel à la base de données si l'utilisateur n'existe pas, car la première fonction garantit que l'utilisateur existe. Maintenant, nous pouvons faire quelque chose comme ça dans l'itinéraire:



app.get('/users/{id}/profiles', handler)
 
// --- //
 
async function handler (req, res) {
  try {
    const userId = req.params.id
    const profile = await userService.getProfile(userId)
    return res.status(200).json(profile)
  } catch (e) {
    if (e instanceof UserNotFoundError || e instanceof ProfileNotFoundError) return res.status(404).json(e.message)
    if (e instanceof InvalidIDError) return res.status(400).json(e.message)
  }
}


Nous pouvons trouver le type d'erreur renvoyé en vérifiant simplement le nom d'instance de la classe d'erreur existante.



Conclusion



Il existe plusieurs façons de traiter les données pour assurer un flux d'informations continu et prévisible. Connaissez-vous d'autres conseils?! Laissez-les dans les commentaires Vous



aimez le matériel?! Envie de donner des conseils, d'exprimer une opinion ou simplement de dire bonjour? Voici comment me trouver sur les réseaux sociaux:








Cet article a été initialement publié sur dev.to par Lucas Santos. Si vous avez des questions ou des commentaires sur le sujet de l'article, postez-les sous l'article original sur dev.to



All Articles