Source: ressource/serviceRessourceControl.js


'use strict'

const _ = require('lodash')
const sjt = require('sesajstools')
const rTools = require('../lib/ressource')

const config = require('./config')
const appConfig = require('../config')
const myBaseId = appConfig.application.baseId
const Ressource = require('../../constructors/Ressource')

module.exports = function (component) {
  component.service('$ressourceControl', function (EntityRessource) {
    /**
     * Ajoute les propriétés qui peuvent être déduites (deductions définies dans la configuration)
     * - categories si vide et que type permet de le deviner
     * - typePedagogiques et typeDocumentaires si categories n'en contient qu'une et qu'elle permet de les déduire
     * @private
     * @param {Ressource} ressource une ressource (ou des datas qui y ressemblent)
     */
    function addDeductions (ressource) {
      // si categories absent on essaie de le déduire de type
      if (ressource && ressource.type && _.isEmpty(ressource.categories) && config.typeToCategories[ressource.type]) {
        ressource.categories = config.typeToCategories[ressource.type]
      }
      // puis typePedagogiques et typeDocumentaires d'après catégories
      if (ressource && ressource.categories && ressource.categories.length) {
        const noTp = _.isEmpty(ressource.typePedagogiques)
        const noTd = _.isEmpty(ressource.typeDocumentaires)
        if (noTp || noTd) {
          // on se penche sur la question
          ressource.categories.forEach(function (categorie) {
            if (config.categoriesToTypes[categorie]) {
              if (noTp) {
                config.categoriesToTypes[categorie].typePedagogiques.forEach(function (tp) {
                  if (!ressource.typePedagogiques.includes(tp)) ressource.typePedagogiques.push(tp)
                })
              }
              if (noTd) {
                if (!Array.isArray(ressource.typeDocumentaires)) ressource.typeDocumentaires = []
                config.categoriesToTypes[categorie].typeDocumentaires.forEach(function (td) {
                  if (!ressource.typeDocumentaires.includes(td)) ressource.typeDocumentaires.push(td)
                })
              }
            }
          })
        }
      }
    }

    /**
     * Ajoute des warnings éventuels (arbre sans enfants ou catégorie mise à 'non défini')
     * @private
     * @param {Ressource} ressource
     */
    function addWarnings (ressource) {
      // catégorie aucune
      if (ressource.categories[0] === config.constantes.categories.aucune) {
        rTools.addWarning(ressource, 'il faudra choisir une catégorie')
      }
      if (ressource.type === 'arbre') {
        // parsing des éventuels enfants
        if (ressource.enfants && ressource.enfants.length) {
          checkEnfants(ressource.enfants, ressource)
        } else if (ressource.oid) {
          // à la création (sans oid) c'est normal
          rTools.addWarning(ressource, 'arbre sans enfants')
        }
      }
    }

    /**
     * Ajoute à la ressource les erreurs rencontrées lors du parsing des enfants
     * @private
     * @param {Array}     enfants
     * @param {Ressource} ressource
     * @param {string}    [titre]   Le titre du parent
     */
    function checkEnfants (enfants, ressource, titre) {
      if (!titre) titre = ressource.titre
      if (Array.isArray(enfants)) {
        const reCheckAliasOf = /^[-\w]+\/[-\w]+$/
        enfants.forEach(function (enfant, i) {
          if (!enfant.titre) rTools.addError(ressource, `L’enfant n° ${i + 1} de « ${titre} » n’a pas de titre`)
          const errPrefix = `L’enfant « ${enfant.titre} » de « ${titre} »`
          if (!enfant.type) rTools.addError(ressource, `${errPrefix} n'a pas de type`)
          if (enfant.type === 'error') return
          if (enfant.aliasOf) {
            if (typeof enfant.aliasOf !== 'string' || !reCheckAliasOf.test(enfant.aliasOf)) {
              rTools.addError(ressource, `${errPrefix} n’a pas un aliasOf valide`)
            }
          } else {
            if (enfant.type === 'arbre') {
              // aliasOf pas obligatoire mais faudrait des enfants
              if (!enfant.enfants) rTools.addWarning(ressource, `${errPrefix} est un arbre sans enfants ni aliasOf`)
            } else {
              rTools.addError(ressource, `${errPrefix} n'a pas d’aliasOf`)
            }
          }
          if (enfant.enfants) {
            checkEnfants(enfant.enfants, ressource, enfant.titre)
          }
        })
      } else {
        rTools.addError(ressource, `enfants de « ${titre} »  invalides (pas une liste)`)
      }
    }

    /**
     * Vérifie que les champs obligatoires existent et sont non vides, et que les autres sont du type attendu
     * Fait du cast sans râler quand les propriétés de ressource sont 'presque" du bon type
     * @memberOf $ressourceControl
     * @param {object} ressource objet qui provient d'un post (toutes les valeurs sont des strings, les boolean sont sous la forme checkbox
     * @param {ressourceCallback} next Callback appelé en synchrone qui recevra les arguments (error, ressource)
     *                        ressource pourra avoir $errors ou $warnings (cast éventuels effectués)
     */
    function valide (data, next) {
      log.debug('ressource dans valide', data, 'form', { max: 2000 })
      if (!next) throw new Error('pas de callback fournie')
      if (_.isEmpty(data)) return next(new Error('Ressource vide'))
      // parsing des propriétés qui pourraient être envoyées en json
      // par ex les propriétés qui étaient hidden
      _.each(data, function (value, prop) {
        // on parse les string si on attendait Array ou Object
        if (typeof value === 'string' &&
          (config.typesVar[prop] === 'Array' || config.typesVar[prop] === 'Object')
        ) {
          try {
            data[prop] = JSON.parse(value)
          } catch (error) {
            rTools.addError(data, `Le contenu de ${prop} était invalide`)
            delete data[prop] // le constructeur initialisera
          }
        }
      })
      // le constructeur fait office de validateur,
      // log.debug('auteurs avant constructeur', data.auteurs)
      const ressource = new Ressource(data, myBaseId)
      // log.debug('auteurs après constructeur', ressource.auteurs)

      // on ajoute les déductions avant check required car des trucs obligatoires peuvent être déduits du type
      addDeductions(ressource)
      // vérif des required
      _.each(config.required, function (required, prop) {
        if (required && _.isEmpty(ressource[prop])) {
          rTools.addError(ressource, `Le champ ${config.labels[prop]} est obligatoire`)
          log.dataError(ressource.rid + ' a une valeur requise manquante : ' + prop + ' => ' + sjt.stringify(ressource[prop]))
        }
      })
      addWarnings(ressource)

      next(null, ressource)
    }

    /**
     * Converti le post reçu en ressource avec cast sur les propriétés et formatage de date
     * Ajoute des choses dans ressource.$warnings ou ressources.errors si besoin (et laisse inchangé les valeurs dans ce cas)
     * @memberOf $ressourceControl
     * @param {Object} data Le post
     * @param {boolean} [partial=false] Passer true pour ne pas générer d'erreur sur des champs requis manquants
     * @param {function} next Si appelé sans error, la ressource est valide,
     *                        sinon y'a une error et des warnings|errors ajouté à la ressource initiale qui est renvoyée modifiée
     */
    function valideRessourceFromPost (data, next) {
      if (_.isEmpty(data)) return next(new Error('Ressource vide'))
      if (data.ressource) {
        // on nous envoie la ressource en json dans une string
        try {
          data = JSON.parse(data.ressource)
        } catch (error) {
          return next(new Error('json invalide dans la propriété ressource postée'))
        }
      }
      // on peut tenter une validation
      valide(data, next)
    }

    /**
     * Service de validation / controle du contenu d'une ressource
     * @service $ressourceControl
     * @requires EntityRessource
     */
    return {
      valide,
      valideRessourceFromPost
    }
  })
}