Source: ressource/serviceRessourceConverter.js


'use strict'
const flow = require('an-flow')
const { isArrayNotEmpty } = require('sesajstools')
const { exists, getBaseUrl, getRidComponents } = require('sesatheque-client/dist/server/sesatheques')
// pour les constantes et les listes, ça reste nettement plus pratique d'accéder directement à l'objet
// car on a l'autocomplétion sur les noms de propriété
const config = require('./config')
const appConfig = require('../config')
const myBaseId = appConfig.application.baseId

/**
 * Service qui regroupe les fonctions de transformation de données sur des ressources
 * (objets vers vue ou résultat de post vers controller)
 * @service $ressourceConverter
 * @requires $ressourceRepository
 * @requires $routes
 * @requires $accessControl
 */
const $ressourceConverter = {}

module.exports = function (component) {
  component.service('$ressourceConverter', function ($ressourceRepository, $routes, $accessControl, $ressourceFetch, $personneRepository) {
    /**
     * Ajoute des relations à une ressource en vérifiant que ce sont des tableau de 2 éléments
     * dont le 1er est un id de relation valide
     * @memberOf $ressourceConverter
     * @param ressource
     * @param relations
     * @returns {string[]} Les erreurs éventuelles, ou false si y'a pas eu d'erreur mais que l'on a rien modifié (la relation y était déjà)
     */
    $ressourceConverter.addRelations = function (ressource, relations) {
      let errors = []
      let isModif = false
      if (Array.isArray(relations)) {
        console.log(`ajout à ${ressource.rid || `${ressource.origine}/${ressource.idOrigine}`} des relations `, relations)
        relations.forEach(relation => {
          if (!Array.isArray(relation)) {
            errors.push(`relation incorrecte : ${JSON.stringify(relation)}`)
            return
          }
          const [relId, relTarget, rest] = relation
          if (rest) {
            errors.push('une relation doit être un tableau à deux éléments [typeRelation, ridRessource]')
          } else if (!config.listes.relations[relId]) {
            errors.push(`Une relation doit être un tableau à deux éléments [typeRelation, ridRessource], ${relId} n’est pas un type de relation valide`)
          } else if (typeof relTarget !== 'string' || relTarget.indexOf('/') < 1) {
            errors.push(`Une relation doit être un tableau à deux éléments [typeRelation, ridRessource], ${relTarget} n’est pas un rid valide`)
          } else if (relTarget === ressource.rid) {
            errors.push('Impossible de lier une ressource à elle-même')
          } else if (relTarget === (ressource.origine + '/' + ressource.idOrigine)) {
            errors.push(`Une relation doit être un tableau à deux éléments [typeRelation, ridRessource], ${relTarget} n’est pas un rid valide`)
          } else {
            // ça semble bon, on regarde si la base est valide

            // et si cette relation n'y est pas déjà...
            const alreadyHere = ressource.relations.find(([exRelId, exRelTarget]) => exRelId === relId && exRelTarget === relTarget)
            if (!alreadyHere) {
              const [baseId, , oups] = relTarget.split('/')
              if (oups) {
                errors.push(`Une relation doit être un tableau à deux éléments [typeRelation, ridRessource], ${relTarget} n’est pas un rid valide`)
              } else if (!exists(baseId)) {
                errors.push(`Une relation doit être un tableau à deux éléments [typeRelation, ridRessource], ${relTarget} n’est pas un rid valide (baseId inconnue)`)
              } else {
                ressource.relations.push([relId, relTarget])
                isModif = true
              }
            }
          }
        })
      } else {
        errors.push('relations incorrectes')
      }

      // si y'a ni erreur ni modifs on return false pour dire qu'il n'y a rien à changer
      if (!errors.length && !isModif) errors = false

      return errors
    }

    /**
     * Ajoute les propriétés urlXXX à chaque elt du tableau de ressource
     * @memberOf $ressourceConverter
     * @param {Ressource[]}   ressources
     * @param {Context} context
     * @returns {Array} ressources
     */
    $ressourceConverter.addUrlsToList = function (ressources, context) {
      if (!ressources) return []
      if (!ressources.length) return []
      return ressources.map((ressource) => {
        try {
          ressource.urlDescribe = $routes.getAbs('describe', ressource)
          ressource.urlPreview = $routes.getAbs('preview', ressource)
          ressource.urlDisplay = $routes.getAbs('display', ressource)
          if (context && $accessControl.hasPermission('update', context, ressource)) {
            ressource.urlEdit = $routes.getAbs('edit', ressource)
          }
          return ressource
        } catch (error) {
          log.error(error)
          return {
            type: 'error',
            titre: error.toString()
          }
        }
      })
    }

    /**
     * Ajoute des infos à la ressource pour résoudre les refs externes, pour la vue describe
     * (nom des auteurs, des ressources liées, etc)
     * _auteurs : string[] avec les noms
     * _contributeurs : idem
     * _enfants : Array de {titre, [oid], [url]}
     * _relations : Array de {predicat, lien, url, titre, rid, type} (tous des strings, lien est le tag a complet)
     * @param {Ressource} ressource
     * @param next
     */
    $ressourceConverter.enhance = function enhance (ressource, next) {
      flow().seq(function () {
        // enfants éventuels, sync
        if (isArrayNotEmpty(ressource.enfants)) {
          ressource._enfants = []
          const enfants = ressource.enfants.filter(e => e)
          if (enfants.length < ressource.enfants.length) log.dataError(`La ressource ${ressource.oid} a des enfants invalides`, enfants)
          enfants.forEach(function (enfant) {
            // ça peut être un dossier seul
            if (!enfant.aliasOf) return ressource._enfants.push({ titre: enfant.titre })
            // sinon on veut le lien
            try {
              const [baseId, oid] = getRidComponents(enfant.aliasOf)
              const url = getBaseUrl(baseId) + $routes.getAbs('describe', oid).substr(1)
              ressource._enfants.push({
                oid,
                titre: enfant.titre,
                url
              })
            } catch (error) {
              log.dataError(`enfant de ${ressource.oid} avec un rid non conforme`, enfant)
            }
          })
        }

        // history, sync
        if (ressource.version > 1) {
          ressource._historyUrl = $routes.getAbs('history', ressource.oid)
        }

        // étape relations
        const nextStep = this
        if (!isArrayNotEmpty(ressource.relations)) {
          log.debug('pas de relations')
          return nextStep()
        }
        log.debug('faut ajouter des titres de relations', ressource.relations)
        ressource._relations = []
        flow(ressource.relations).seqEach(function ([relationId, relationTarget]) {
          const nextRelation = this
          $ressourceFetch.fetch(relationTarget, function (error, ressourceLiee) {
            if (error) {
              log.error(error)
            } else if (ressourceLiee) {
              if (!ressourceLiee.rid) {
                log.dataError(`la ressource ${relationTarget} n’a pas de rid !`, ressourceLiee)
                return nextRelation()
              }
              const [baseId, oid] = getRidComponents(ressourceLiee.rid)
              ressource._relations.push({
                predicat: config.listes.relations[relationId],
                // pour le template html
                lien: $routes.getTagA('describe', ressourceLiee),
                // pour l'api
                url: getBaseUrl(baseId) + $routes.getAbs('describe', oid).substr(1),
                titre: ressourceLiee.titre,
                rid: ressourceLiee.rid,
                type: ressourceLiee.type
              })
            } else {
              log.dataError(`la ressource ${ressource.oid} est liée à ${relationTarget} qui n’existe pas`)
            }
            nextRelation()
          })
        }).seq(function () {
          // log.debug('on a ajouté les titres des relations', ressource._relations)
          nextStep()
        }).catch(function (e) {
          log.error(e)
          nextStep()
        })
      }).seq(function () {
        // auteurs
        const nextStep = this
        if (!isArrayNotEmpty(ressource.auteurs)) return nextStep()
        // y'a des auteurs
        ressource._auteurs = []
        flow(ressource.auteurs).seqEach(function (pid) {
          const nextAuteur = this
          $personneRepository.load(pid, function (error, personne) {
            if (error) log.error(error)
            else if (personne) ressource._auteurs.push(`${personne.prenom} ${personne.nom}`)
            else ressource._auteurs.push(`auteur ${pid} inconnu`)
            nextAuteur()
          })
        }).seq(function () {
          nextStep()
        }).catch(function (error) {
          log.error('erreur dans le flux auteurs de la ressource ' + ressource.oid, error)
          nextStep()
        })
      }).seq(function () {
        // contributeurs
        const nextStep = this
        if (!isArrayNotEmpty(ressource.contributeurs)) return nextStep()
        ressource._contributeurs = []
        flow(ressource.contributeurs).seqEach(function (pid) {
          const nextContributeur = this
          $personneRepository.load(pid, function (error, personne) {
            if (error) log.error(error)
            else if (personne) ressource._contributeurs.push(`${personne.prenom} ${personne.nom}`)
            else ressource._contributeurs.push(`contributeur ${pid} inconnu`)
            nextContributeur()
          })
        }).seq(function () {
          nextStep()
        }).catch(function (error) {
          log.error('erreur dans le flux contributeurs de la ressource ' + ressource.oid, error)
          nextStep()
        })
      }).seq(function () {
        // on a tout, on peut envoyer
        next(null, ressource)
      }).catch(function (error) {
        // en cas d'erreur dans le flux on envoie quand même la ressource en l'état
        log.error(`erreur dans la recherche des références externes de la ressource ${ressource.oid}`, error)
        next(error, ressource)
      })
    }

    /**
     * Helper de GET /ressource/modifier/:oid et /api/ressource/:oid/forkAlias
     * pour transformer un alias en ressource autonome (quand on édite cet alias)
     * @param {Context} context
     * @param {Ressource} ressource
     * @param {callbackRessource}
     */
    $ressourceConverter.forkAlias = function (myPid, ressource, callback) {
      flow().seq(function () {
        if (!ressource.aliasOf) throw new Error('Impossible de dupliquer un alias qui n’en est pas un')
        if (!config.editable[ressource.type]) throw new Error(`Le type de ressource ${ressource.type} n’est pas modifiable`)
        // on édite un alias, faut récupérer l'ensemble des datas de l'original pour
        // en faire une vraie ressource (un fork de l'original)
        $ressourceFetch.fetchOriginal(ressource.aliasOf, this)
      }).seq(function (ressourceOriginale) {
        if (!ressourceOriginale) {
          // ce cas devrait être exclu (si l'original avait disparu on aurait dû être prévenu)
          // mais vaut mieux vérifier
          log.error(`fetchOriginal(${ressource.aliasOf}) ne renvoie ni ressource ni erreur`)
          return callback(null)
        }

        // on peut forker en partant sur la base de l'alias
        const forcedProps = {
          oid: ressource.oid,
          rid: ressource.rid,
          // faudrait lui générer une nouvelle clé si besoin, le beforeStore s'en chargera, il suffit de lui virer l'actuelle
          cle: undefined,
          origine: myBaseId,
          idOrigine: ressource.oid,
          auteurs: [myPid],
          version: 1
        }
        const fork = Object.assign({}, ressourceOriginale, forcedProps)
        const pids = ressourceOriginale.auteurs.concat(ressourceOriginale.auteursParents).filter(pid => pid !== myPid)
        // un set pour dédup
        fork.auteursParents = Array.from(new Set(pids))
        // c'est plus un alias…
        delete fork.aliasOf
        // … mais une ressource liée
        if (!fork.relations) fork.relations = []
        fork.relations.push([config.constantes.relations.estVersionDe, ressourceOriginale.rid])
        $ressourceRepository.save(fork, this)
      }).seq(function (forkedRessource) {
        callback(null, forkedRessource)
      }).catch(function (error) {
        log.error(error)
        callback(error)
      })
    }

    // noinspection FunctionWithMoreThanThreeNegationsJS
    /**
     * Peuple les enfants d'un arbre en allant les chercher en bdd
     * @memberOf $ressourceConverter
     * @param {Context} context
     * @param ressource
     * @param next
     */
    $ressourceConverter.populateArbre = function (context, ressource, next) {
      /**
       * Parcours les enfants de parent pour les transformer et appeler nextStep
       * (sans argument, nextStep peut être le this de seq)
       * @private
       * @param parent
       * @param nextStep
       */
      function populateEnfants (parent, nextStep) {
        /**
         * Met à jour un enfant chez son parent d'après une ressource récupérée en bdd
         * @private
         * @param enfantIndex
         * @param ressourceBdd
         * @param next
         */
        function updateEnfant (enfantIndex, ressourceBdd, next) {
          const enfant = parent.enfants[enfantIndex]
          if (ressourceBdd) {
            const newEnfant = {
              oid: ressourceBdd.oid,
              titre: ressourceBdd.titre,
              type: ressourceBdd.type
            }
            if (enfant.contenu) newEnfant.contenu = enfant.contenu
            if (enfant.enfants && enfant.enfants.length) newEnfant.enfants = enfant.enfants
            // visiblement seq casse les références, on affecte directement à la variable parent restée hors du flux
            parent.enfants[enfantIndex] = newEnfant
          } else {
            // sinon on laisse en l'état mais on logue
            log.dataError('On a pas trouvé la ressource ' + enfant.idOrigine + ' ' + enfant.id)
            parent.enfants[enfantIndex].titre += ' (non trouvé)'
          }
          populateEnfants(parent.enfants[enfantIndex], next)
        }

        if (parent.enfants && parent.enfants.length) {
          flow(parent.enfants).seqEach(function (enfant, enfantIndex) {
            const finEach = this
            // pour permettre de récupérer des objets d'après leur ref d'origine, on accepte aussi id et idOrigine (à la place de ref)
            if (enfant.origine && enfant.idOrigine) {
              // on le cherche en db
              // const logSuffix = enfant.idOrigine + ' - ' + enfant.id
              // log('load ' + logSuffix)
              $ressourceRepository.loadByOrigin(enfant.origine, enfant.idOrigine, function (error, ressource) {
                if (error) log.error(error)
                log.debug('on traite l’enfant ' + enfant.titre + ' et on a récupéré ', ressource)
                updateEnfant(enfantIndex, ressource, finEach)
              })
            } else if (enfant.aliasOf) {
              $ressourceRepository.load(enfant.aliasOf, function (error, ressource) {
                if (error) log.error(error)
                updateEnfant(enfantIndex, ressource, finEach)
              })
            } else {
              // pas de ref ni idOrigine
              populateEnfants(enfant, finEach)
            }
          }).seq(function () {
            nextStep()
          }).catch(function (error) {
            log.error(new Error("L'analyse de l'arbre a planté (cf aussi erreur suivante)"), parent)
            log.error(error)
            nextStep()
          })
        } else {
          nextStep()
        }
      } // populateEnfants

      // checks
      if (ressource.type !== 'arbre') {
        next(new Error('Méthode réservée au type arbre'))
      } else if (!isArrayNotEmpty(ressource.enfants)) {
        log.debug('arbre vide', ressource)
        next(new Error('Impossible de peupler un arbre vide'))
      } else {
        // go
        populateEnfants(ressource, next)
      }
    }

    return $ressourceConverter
  })
}