Source: ressource/controllerApiListe.js


'use strict'
const flow = require('an-flow')
const { pick } = require('lodash')
const config = require('../config')
const configRessource = require('./config')
const Ref = require('../../constructors/Ref')
const Ressource = require('../../constructors/Ressource')
const url = require('../lib/url')

const myBaseUrl = config.application.baseUrl
const updateUrl = (context, options) => myBaseUrl + url.update(context.request.originalUrl, options).substr(1)

const { listeMax, listeNbDefault } = configRessource.limites
if (!listeMax) throw new Error('settings.ressource.limites.listeMax manquant')
/**
 * Controleur de la route /api/ (qui répond en json) pour les ressources
 * Toutes les routes contenant /public/ ignorent la session (cookies viré par varnish,
 * cela permet de mettre le résultat en cache et devrait être privilégié pour les ressources publiques)
 *
 * Tout ce qui renvoie une ressource (ou un oid pour du post) accepte en queryString
 * - format=(ref|ressource|full) ref renvoie une ref (toujours avec les _droits), full ajoute la résolution des id (auteurs, relations, groupes…)
 * - droits=1 pour ajouter une propriété _droits (string contenant des lettres parmi RWD)
 *
 * @Controller controllerApi
 */
module.exports = function (component) {
  component.controller('api/liste', function ($ressourceRepository, $ressourceConverter, $ressourceControl, $accessControl, $personneControl, $personneRepository, $json) {
    /**
     * Envoie une liste de ressources (ajoute les droits)
     * @private
     * @param {Context} context
     * @param {Error} [error]
     * @param {EntityRessource[]} ressources La liste des ressources
     * @param {object} listOptions
     * @param {object} listOptions.query La query qui a donné cette liste
     * @param {object} listOptions.queryOptions Les queryOptions (skip, limit, orderBy) utilisés
     * @param {number} listOptions.total
     * @param {string[]} [listOptions.warnings]
     */
    function sendListe (context, error, ressources, { query, queryOptions, total }) {
      if (error) return $json.sendKo(context, error)
      // @todo virer ça dès que tout le monde nous appellera avec ces infos
      if (!queryOptions) queryOptions = {}
      if (total === undefined) total = ressources.length

      const liste = []
      const reponse = { query, queryOptions, total, liste }
      if (ressources && ressources.length) {
        // construction de nextUrl
        const limit = queryOptions.limit || Number(context.get.limit) || listeNbDefault
        if (ressources.length === limit) {
          const skip = (queryOptions.skip || Number(context.get.skip) || 0) + limit
          reponse.nextUrl = updateUrl(context, { ...query, skip })
        }
        // on regarde le format reçu en get ou post
        const format = context.post.format || context.get.format || 'ref'
        ressources.forEach(function (ressource) {
          // vérif des droits
          let droits = ''
          const readErrorMessage = $accessControl.getReadDeniedMessage(context, ressource)
          if (readErrorMessage) {
            // ça devrait pas arriver, mais au cas où on crée une ressource fake
            // pour avoir le bon nb dans la liste et montrer le pb
            log.error(`sendListe récupère la ressource ${ressource.oid} à envoyer à ${$accessControl.getCurrentUserPid(context)} alors qu’il n’a pas les droits de lecture dessus`, ressource)
            ressource = new Ressource({
              titre: readErrorMessage,
              type: 'error'
            })
            // faut un oid (sinon react est pas content car oid est obligatoire)
            ressource.oid = ''
          } else {
            droits += 'R'
            if ($accessControl.hasPermission('update', context, ressource)) droits += 'W'
            if ($accessControl.hasPermission('delete', context, ressource)) droits += 'D'
          }

          // formatage
          let item
          if (format === 'full') {
            item = ressource
          } else if (format === 'light') {
            item = pick(ressource, ['oid', 'titre', 'type', 'resume', 'rid', 'description', 'commentaires'])
          } else {
            item = new Ref(ressource)
            // on rajoute les parametres pour les sequenceModele
            if (ressource.type === 'sequenceModele') item.parametres = ressource.parametres
          }
          item.$droits = droits
          liste.push(item)
        })
      }
      $json.sendOk(context, reponse)
    }

    const controller = this

    /**
     * Récupère des résultats de recherche
     * @route GET /api/liste
     */
    controller.get('', function (context) {
      // on vérifie les paramètres pour construire query et queryOptions
      const params = $accessControl.sanitizeSearch(context)

      $ressourceRepository.search(params.query, params.queryOptions, function (error, result) {
        if (error) return $json.sendKo(context, error)

        const { ressources, total } = result
        params.total = total
        sendListe(context, error, ressources, params)
      })
    })

    /**
     * Récupère les ressources d'une liste de pids, classée par pid (avec prénom & nom du pid)
     * Retourne {@link reponseListesByPid}
     * Utilisé par sesathequeClient.getListeAuteurs()
     * @route GET /api/liste/auteurs
     * @param {string} pids la liste de pids séparés par des virgules
     */
    controller.get('auteurs', function (context) {
      const pids = context.get.pids && context.get.pids.split(',').filter(pid => pid.indexOf('/') > 0)
      if (!pids) return $json.sendKo(context, 'Argument pids manquant')
      if (!pids.length) return $json.sendKo(context, 'Aucun auteur demandé')
      const { queryOptions } = $accessControl.sanitizeSearch(context)
      const { limit, skip } = queryOptions
      const query = { auteurs: [] }

      let nbRessources = 0 // pour savoir s'il faut du nextUrl
      let nbForbidden = 0
      let nbLost = 0

      const retour = { warnings: [] }
      flow().seq(function () {
        // grab auteurs
        $personneRepository.loadByPids(pids, this)
      }).seqEach(function (personne, index) {
        if (personne) {
          retour[personne.pid] = {
            pid: personne.pid,
            label: `${personne.prenom} ${personne.nom}`,
            liste: []
          }
          query.auteurs.push(personne.pid)
        } else {
          retour.warnings.push(`Auteur ${pids[index]} inconnu`)
        }

        // grab ressources
        $ressourceRepository.search(query, queryOptions, this)
      }).seq(function ({ ressources, total }) {
        retour.total = total
        if (!total) return $json.sendOk(context, retour)

        ressources.forEach((ressource) => {
          nbRessources++
          if ($accessControl.hasReadPermission(context, ressource)) {
            ressource.auteurs.forEach(pid => {
              if (retour[pid]) retour[pid].liste.push(new Ref(ressource))
              else nbLost++
            })
          } else {
            nbForbidden++
          }
        })

        if (nbForbidden) retour.warnings.push(`${nbForbidden} ressources supprimées de la liste car vous n’aviez pas les droits de lecture dessus`)
        if (nbLost) retour.warnings.push(`${nbLost} ressources supprimées car leur auteur n’existe plus`)
        if (!retour.warnings.length) delete retour.warnings

        if (nbRessources === listeMax) retour.nextUrl = updateUrl(context, { skip: skip + limit })
        if (context.get.skip > 0) retour.prevUrl = updateUrl(context, { skip: Math.max(skip - limit, 0) })

        $json.sendOk(context, retour)
      }).catch(function (error) {
        $json.sendKo(context, error)
      })
    })

    /**
     * Cherche parmi les ressources du user courant (qui doit être connecté avant)
     * Retourne {@link reponseListe}
     * @route GET /api/liste/perso
     * @param {requeteListe}
     */
    controller.get('perso', function (context) {
      context.timeout = 3000
      const pid = $accessControl.getCurrentUserPid(context)
      if (!pid) return $json.denied(context, 'Ressources personnelles inaccessibles (session expirée sur la Sésathèque), veuillez vous déconnecter et reconnecter')

      const { queryOptions } = $accessControl.sanitizeSearch(context)
      const query = { auteurs: [pid] }
      flow().seq(function () {
        $ressourceRepository.search(query, queryOptions, this)
      }).seq(function ({ ressources, total }) {
        sendListe(context, null, ressources, { total, query, queryOptions })
      }).catch(function (error) {
        $json.sendKo(context, error)
      })
    })
  })
}

/**
 * Format de la réponse à une demande de liste
 * @typedef reponseListe
 * @type {Object}
 * @property {boolean}                     success
 * @property {string}                      [error] Message d'erreur éventuel
 * @property {Ref[]|Ressource[]} liste   Une liste de Ref (ou de Ressource si on le demande)
 */

/**
 * Format de la réponse à une demande de liste
 * @typedef reponseListesByPid
 * @type {Object}
 * @property {boolean} success
 * @property {string}   [error] Message d'erreur éventuel
 * @property {string[]} [warnings] Éventuelle liste de warnings (auteurs inconnus)
 * @property {object}   pidXX   Objet contenant les ressources de pidXX
 * @property {string}   pidXX.pid   rappel de son pid
 * @property {string}   pidXX.label prénom & nom
 * @property {Ref[]}    pidXX.liste Ses ressources (lisibles par le demandeur)
 */

/**
 * Arguments à donner à une requête qui renvoie une liste de ressources
 * @typedef requeteListe
 * @type {Object}
 * @property {string}             [json]    Tous les paramètres qui suivent dans une chaîne json (GET seulement, ignoré en POST)
 * @property {requeteArgFilter[]} [filters] Les filtres à appliquer
 * @property {string}             [orderBy] Un nom d'index
 * @property {string}             [order]   Préciser 'desc' si on veut l'ordre descendant
 * @property {Integer}            [start]   offset
 * @property {Integer}            [nb]      Nombre de résultats voulus (Cf settings.ressource.limites.listeNbDefault, à priori 25),
 *                                          sera ramené à settings.ressource.limites.maxSql si supérieur (à priori 500)
 * @property {string}             [format]  ref|full par défaut on remonte les ressource au format {@link Ref}
 */

/**
 * Format d'un filtre à passer à une requete de demande de liste
 * @typedef requeteArgFilter
 * @type {Object}
 * @property {string} index  Le nom de l'index
 * @property {Array}  [values] Une liste de valeurs à chercher (avec des ou), remontera toutes les ressource ayant l'index si omis
 */