Source: cli/checkGraphes.js

/**
 * Ce script passe en revue les ressources j3p (dans la base) pour vérifier que les graphes sont valides
 */
'use strict'

const log = require('an-log')('sesatheque-cli')
const flow = require('an-flow')
const { hasProp, stringify } = require('sesajstools')
const { application: { baseUrl } } = require('../config')

function getNbSectionNodes (graphe) {
  return graphe.filter(node => node.length === 3 && node[1] !== 'fin').length
}

/**
 * Fonction getNormalizedGraphe de j3p (methodesmodele au 14/05/2020) dont on a juste modifié les lignes de log
 * Retourne un graphe normalisé (id en string, nn et snn aussi, fin en minuscule, nœuds fin ajoutés avec nn et snn toujours des id, jamais fin)
 * Retournera null en cas de pb. Attention, graphe peut être modifié également.
 * @param {Ressource} ressource
 * @return {Graphe} Le graphe (avec 1er elt vide) normalisé
 */
function getNormalizedGraphe (ressource) {
  if (!ressource || typeof ressource !== 'object') return log.error(Error('pas de ressource'))
  const { type, oid, parametres } = ressource
  const url = `${baseUrl}ressource/modifier/${oid}`
  const grapheOriginal = stringify(parametres && parametres.g)
  const logError = (message) => log.error(`Erreur ${url} : ${message}`, graphe ? grapheOriginal : '')
  if (type !== 'j3p') return logError('pas de type j3p')
  if (!parametres || typeof parametres !== 'object') return logError('pas de graphe')
  const graphe = parametres.g

  // la suite est la fct d'origine où seuls les logs sont modifiés
  try {
    if (!Array.isArray(graphe)) throw Error(`graphe invalide (pas un array : ${typeof graphe}) : ${stringify(parametres)}`)
    if (!graphe.length) throw Error('graphe vide')
    // chaque élément suivant du graphe est un nœud, il doit être un array avec
    // - le numéro du nœud en string en 1er
    // - un nombre variable de branchements, des object (nombre éventuellement nul, mais à priori au moins un nœud fin)
    // - en dernier un objet avec le paramétrage du nœud
    const cleanGraphe = []
    graphe.forEach(function (noeud, index) {
      // on vire les éléments non Array
      if (!Array.isArray(noeud)) {
        throw Error('noeud d’index ' + index + ' invalide (pas un array)')
      }
      // on ajoute le 1er élément vide s'il n'y était pas, ça devrait devenir inutile depuis qu'on a des id et index clairement distincts,
      // mais il reste du code qui utilise l'id comme index de tableau
      if (index === 0) {
        cleanGraphe.push([])
        // si y'avait déjà le nœud vide en 0, on arrête là
        if (!noeud.length) return
      }
      // on accepte un number pour le nodeId (nodeNuméro) => cast en string
      if (typeof noeud[0] === 'number') noeud[0] = String(noeud[0])
      // le 2e doit être le nom de la section ou fin ou Fin
      // on nettoie d'abord les nœuds fin, car on peut trouver du [null] pour les options
      const isFin = typeof noeud[1] === 'string' && noeud[1].toLowerCase() === 'fin'
      if (isFin) {
        // on fixe en minuscule si c'est pas le cas
        if (noeud[1] !== 'fin') noeud[1] = 'fin'
        // et on vire le reste qui sert à rien
        while (noeud.length > 2) noeud.pop()
      }
      if (
        noeud[0] &&
        typeof noeud[0] === 'string' &&
        noeud[1] &&
        typeof noeud[1] === 'string' &&
        // si y'a un 3e elt ça doit être un tableau d'objet
        (
          isFin ||
          (Array.isArray(noeud[2]) && noeud[2].every(function (elt) { return elt && typeof elt === 'object' }))
        )
      ) {
        // le cast des nn & snn en strings se fait avec l'ajout des nœuds fin, juste après
        cleanGraphe.push(noeud)
      } else {
        logError('noeud d’index ' + index + ' invalide')
      }
    })

    // on regarde si tous les branchements pointent qq part, et on ajoute les nœuds fin éventuels (tous les nn & snn seront bien des ids)
    const ids = []
    let nextNum = 1
    let id
    const sectionsNodes = cleanGraphe.filter(function (noeud, index) {
      if (index === 0) return false
      ids.push(noeud[0])
      if (noeud[0] >= nextNum) nextNum = Number(noeud[0]) + 1
      return noeud[1] !== 'fin'
    })
    sectionsNodes.forEach(function (node) {
      const numNode = node[0]
      // le dernier est éventuellement le paramétrage, mais pas forcément (il peut ne pas y avoir de paramétrage)
      const nbParams = Array.isArray(node[2]) && node[2].length
      if (nbParams) {
        node[2].forEach(function (br, i) {
          if (br.pe === '>=0') {
            logError('branchement avec la condition pe >= 0 qui est à éviter (si pe n’est pas numérique ça plante, et sinon ça revient au même que sans condition)')
            delete br.pe
            br.score = 'sans+condition'
          }
          if (typeof br.nn === 'number') br.nn = String(br.nn)
          if (typeof br.nn === 'string') {
            if (br.nn.toLowerCase() === 'fin') {
              // on ajoute un nœud fin
              id = String(nextNum)
              nextNum++
              br.nn = id
              cleanGraphe.push([id, 'fin'])
              ids.push(id)
            } else if (ids.indexOf(br.nn) === -1) {
              logError('Le branchement d’index ' + i + ' du nœud n° ' + numNode + ' pointe vers ' + br.nn + ' qui n’existe pas')
            } else {
              // on vérifie aussi que la condition existe
              if (!hasProp(br, 'pe') && !hasProp(br, 'score')) logError('Le branchement d’index ' + i + ' du nœud n° ' + numNode + ' n’a pas de condition (ni pe ni score)')
            }
          } else if (i !== nbParams - 1) { // le dernier elt peut être le paramétrage
            logError('Le branchement d’index ' + i + ' du nœud n° ' + numNode + ' est invalide : ' + stringify(br))
          }
          // idem pour snn
          if (typeof br.snn === 'number') br.snn = String(br.snn)
          if (typeof br.snn === 'string') {
            if (br.snn.toLowerCase() === 'fin') {
              // on ajoute un nœud fin
              id = String(nextNum)
              nextNum++
              br.snn = id
              cleanGraphe.push([id, 'fin'])
              ids.push(id)
            } else if (ids.indexOf(br.snn) === -1) {
              logError('Le branchement sinon d’index ' + i + ' du nœud n° ' + numNode + ' pointe vers ' + br.snn + ' qui n’existe pas')
            }
          }
        })
      } else {
        // on plante pas mais on le signale
        logError('nœud n° ' + node[0] + ' sans branchement')
      }
    })
    if (getNbSectionNodes(cleanGraphe) < 1) throw Error('graphe sans section')
    return cleanGraphe
  } catch (error) {
    logError(error.message)
    return null
  }
} // getNormalizedGraphe

function checkGraphes (oid, done) {
  function grab (next) {
    flow().seq(function () {
      log(`traitement des ressources j3p de ${offset} à ${offset + limit} (sur ${nbRess})`)
      EntityRessource.match('type').equals('j3p').sort('dateCreation').grab({ limit, offset }, this)
    }).seqEach(function (ress) {
      checkOne(ress, this)
    }).seq(function (results) {
      if (results.length === limit) {
        offset += limit
        process.nextTick(() => grab(next))
      } else {
        next()
      }
    }).catch(next)
  } // grab

  function checkOne (ress, next) {
    const { oid, type, parametres } = ress
    if (type !== 'j3p') {
      log.error(`${oid} n’est pas une ressource j3p`)
      return next()
    }

    if (parametres && parametres.xml) {
      // faut passer par $ressourceRepository pour qu'il convertisse (et sauvegarde la modif en tâche de fond, il ne le faisait pas avant le 13/05/2020)
      log(`Ressource ${oid} avec du xml, on lance la conversion`)
      $ressourceRepository.save(ress, (error, ress) => {
        if (error) log.error(`Pb avec la ressource ${oid}`, error)
        else if (ress) getNormalizedGraphe(ress)
        else log.error(Error(`On a récupéré une ressource via EntityRessource ${oid} que $ressourceRepository ne remonte pas`))
        // faut laisser le temps à mongo d'écrire ça avant de passer à la suite, on attends arbitrairement 500ms
        setTimeout(next, 500)
      })
    } else {
      getNormalizedGraphe(ress)
      next()
    }
  }

  // check préalable & init
  if (typeof oid === 'function') {
    done = oid
    oid = undefined
  }
  if (typeof done !== 'function') throw new Error('Erreur interne, pas de callback de commande')
  let offset = 0
  const limit = 50
  let nbRess = 0
  const EntityRessource = lassi.service('EntityRessource')
  const $ressourceRepository = lassi.service('$ressourceRepository')

  // go
  if (oid) {
    log(`Starting checkGraphes ${oid}`)
    flow().seq(function () {
      EntityRessource.match('oid').equals(oid).grabOne(this)
    }).seq(function (ress) {
      if (!ress) {
        log(`La ressource ${oid} n’existe pas`)
        return done()
      }
      checkOne(ress, this)
    }).seq(function () {
      log(`fin du check de ${oid}`)
      done()
    }).catch(done)
  } else {
    flow().seq(function () {
      EntityRessource.match('type').equals('j3p').count(this)
    }).seq(function (nb) {
      nbRess = nb
      log(`Starting checkGraphes avec ${nb} ressources j3p à analyser`)
      grab(this)
    }).seq(function () {
      log(`fin du check de ${nbRess} ressources j3p`)
      done()
    }).catch(done)
  }
}

checkGraphes.help = function checkGraphesHelp () {
  log('La commande checkGraphes prend un oid en argument pour vérifier le graphe de la ressource, sans argument elle lance la vérification des graphes de toutes les ressources de type j3p')
}

module.exports = {
  checkGraphes
}