import { addSesalabSsoToken, callApi, callApiUrl, extractBaseUrl, hasToken } from './internals'
import { addSesatheque, addSesatheques, exists, getBaseId, getBaseUrl, getComponents, reBaseUrl } from './sesatheques'
import { highLimit } from './fetch'
import ClientItem from './constructors/ClientItem'
import Ressource from './constructors/Ressource'
import config from './config'
import uuid from 'an-uuid'
import queryString from 'query-string'
import filters from 'sesajstools/utils/filters'
import log from 'sesajstools/utils/log'
import { complete, getParameter } from 'sesajstools/http/url'
// on réexporte ça
export { addSesatheque, addSesatheques, exists, reBaseUrl, ClientItem }
// on met un hardLimit sur le nb de xhr passés en boucle
const hardLimitXhrCalls = 100
let timeout = 10000
// pour les get qui veulent un objet
const wantDataOptions = { isDataExpected: true }
let debugMode = false
if (typeof window !== 'undefined') debugMode = getParameter('debug')
// debugMode = 'all'
if (debugMode === 'all' || debugMode === 'client') {
log.setLogLevel('debug')
log.debug('Logs de debug activés (sesathèque-client)')
}
/**
* Fournit des méthodes pour appeler la bibliotheque et récupérer des items normalisés (avec des url absolues)
* Initialiser le client avec {@link .getClient} puis utiliser dessus les autres méthodes
* @service sesathequeClient
*/
/**
* Le nom de domaine qui utilise ce client (sans protocole ni port)
* @private
* @type {string}
*/
let origine
/**
* Réalise une action sur l'api et renvoie un feedback
* @private
* @param {string} baseId
* @param {string} apiPath
* @param {string} successMessage
* @param {string} errorMessage
* @param {feedbackCallback} next
*/
function action (baseId, apiPath, successMessage, errorMessage, next) {
callApi(baseId, apiPath, function (error, response) {
if (error) next(errorMessage + ` (${error.toString()})`, 'error')
else if (response.success) next(successMessage)
else next(errorMessage, 'error')
})
}
/**
* Ajoute un listener sur un message {action:'iframeCloser', id:xxx}
* @private
* @param baseUrl
* @param itemCallback
* @returns {*}
*/
function addCloser (baseUrl, itemCallback) {
if (typeof window === 'undefined') {
console.error(Error('addCloser ne peut être utilisé que dans un navigateur'))
return
}
// on ajoute le listener, faut un id car on peut être dans un dom avec plusieurs onglets d'édition possibles
const id = uuid()
if (typeof window.addEventListener === 'undefined') window.addEventListener = function (e, f) { window.attachEvent('on' + e, f) }
window.addEventListener('message', function (event) {
// iframeCloser est en dur dans le contrôleur de la sesatheque
if (event.data && event.data.id === id && event.data.action === 'iframeCloser') {
const data = event.data
if (data.error) {
itemCallback(Error(data.error))
} else if (data.ressource) {
itemCallback(null, normalize(data.ressource))
} else if (data.groupe) {
const name = data.groupe.nom
// pas de normalize, ce serait compliqué avec l'oid du gestionnaire que l'on ne sait pas rapprocher du user
// on est forcément proprio car on vient de le créer
if (name) itemCallback(null, { name: name, $publish: true, $urlAdmin: baseUrl + 'groupe/modifier/' + encodeURIComponent(name) })
else itemCallback(Error('L’enregistrement du groupe n’a pas renvoyé de nom'))
} else {
itemCallback(Error("La réponse n'est pas au format attendu, elle est impossible à interpréter"))
}
} // sinon c'est pas pour nous
})
return id
}
/**
* Retourne l'url absolue, avec skip et limit si besoin
* @param {string} baseId
* @param {string} path le chemin, sans slash de début ni le ? de la queryString
* @param {Object} [options]
* @param {number} [options.skip]
* @param {number} [options.limit]
* @return {string} L'url absolue, avec le ? de la queryString (même si y'en a pas)
*/
function getUrl (baseId, path, options) {
const url = getBaseUrl(baseId) + path
if (options.skip || options.limit) {
const validOptions = {}
if (options.limit) validOptions.limit = options.limit
if (options.skip) validOptions.skip = options.skip
return complete(url, validOptions)
}
return url
}
/**
* Renvoie les groupes demandés
* @private
* @param {string} baseId
* @param {string} apiPath par ex groupe/admin
* @param {string} prop le nom de la propriété à lire dans la réponse (un peu redondant avec apiPath,
* pour groupe/admin c'est groupesAdmin) mais ici on a pas l'association des deux,
* et ça permet une vérif pour limiter les conséquences d'un bug)
* @param next
*/
function getGroupes (baseId, apiPath, prop, next) {
callApi(baseId, apiPath, function (error, response) {
if (error) return next(error)
const liste = response[prop]
if (!liste) return next(Error(`Pas trouvé la liste ${prop} attendue dans la réponse du serveur`))
if (!Array.isArray(liste)) return next(Error(`Réponse malformée (${prop} n’est pas une liste)`))
const groupes = []
liste.forEach(function (groupe) {
if (groupe.name) {
const n = groupe.name
if (groupe.admin) groupes.push({ name: n, $publish: true, $urlAdmin: getBaseUrl(baseId) + 'groupe/modifier/' + encodeURIComponent(n) })
else if (groupe.member) groupes.push({ name: n, $publish: true, $canQuit: true })
else if (groupe.follower) groupes.push({ name: n, $publish: false, $canIgnore: true })
else notifyError(baseId, 'groupe remonté mais sans flag admin ni member ni follower', groupe)
} else {
notifyError(baseId, 'groupe sans nom', groupe)
}
})
next(null, groupes)
})
}
/**
* Retourne une liste d'items
* @private
* @param {string} url
* @param {Object} [options]
* @param {number} [options.limit] À préciser si y'a un nombre max qu'on ne veut pas dépasser
* (attention ça tronque, le fullResponse.nextUrl ne correspondra
* pas à tous les items manquants)
* @param {number} [options.maxCalls] À préciser si y'a un nombre max d'appels qu'on ne veut pas dépasser
* (pour la suite il faudra rappeler getListe avec le fullResponse.nextUrl)
* @param {itemsCallback} next appelé avec (error, items, fullResponse)
*/
function getListe (url, options, next) {
// récupère la liste et se rappelle si nécessaire (et voulu)
function fetchNextChunk (url) {
callApiUrl(url, wantDataOptions, function (error, data) {
if (error) return next(error)
nbCalls++
// le cas attendu
if (data.liste) {
// on récupère
log.debug(`fetch n° ${nbCalls} remonte`, data.liste)
liste = liste.concat(data.liste)
const maxReached = options.max && liste.length >= options.max
const maxCallsReached = nbCalls === options.maxCalls
if (nbCalls === hardLimitXhrCalls) console.error(Error(`Nombre maximum d’appels xhr chaînés automatiquement atteint (${hardLimitXhrCalls})`))
// on regarde si y'en a d'autre
if (data.nextUrl) {
if (!maxReached && !maxCallsReached) return fetchNextChunk(data.nextUrl)
}
if (maxReached) liste = liste.slice(0, options.max)
return next(null, liste.map(normalize), data)
}
// garbage collector
next(Error(`${url} n’a pas renvoyé la liste attendue`))
})
}
if (typeof options === 'function') {
next = options
options = {}
}
let liste = []
let nbCalls = 0
log.debug(`getListe ${url} avec les options`, options)
if (!options.maxCalls || options.maxCalls > hardLimitXhrCalls) options.maxCalls = hardLimitXhrCalls
fetchNextChunk(url)
}
/**
* Helper de saveSequenceModele et saveSerie
* @private
* @param {string} baseId
* @param {Ressource} ressource
* @param {itemCallback} next appelée avec une erreur ou un item valide
*/
function saveRessource (baseId, ressource, next) {
try {
log.debug('saveRessource va envoyer', ressource)
const url = getBaseUrl(baseId) + 'api/ressource?format=ref'
const options = {
body: ressource,
method: 'post',
isDataExpected: true
}
// log.debug('options de saveRessource', options)
callApiUrl(url, options, function (error, ref) {
if (error) return next(error)
if (ref && ref.aliasOf) {
const item = normalize(ref)
if (item && item.rid) return next(null, item)
}
// y'a un truc louche
log.error(Error('réponse inattendue au saveRessource vers ' + baseId), ref)
next(Error('La sauvegarde de la ressource a échouée'))
})
} catch (error) {
log.error(error)
next(error)
}
}
// /////////////////////
// Méthodes exportées
// /////////////////////
/**
* Pour ajouter un item via le form de la sesatheque, retourne l'url à ouvrir en iframe puis appellera itemCallback
* avec l'item sauvegardé (quand la ressource aura été sauvegardée si le user va au bout)
* @param {string} [baseId] l'id de la sésathèque
* @param {itemCallback} itemCallback
* @returns {string} L'url à ouvrir en iframe
*/
export function addItem (baseId, itemCallback) {
const baseUrl = getBaseUrl(baseId)
const id = addCloser(baseUrl, itemCallback)
return baseUrl + 'ressource/ajouter?layout=iframe&closerId=' + id
}
/**
* Pour ajouter un groupe, retourne l'url à ouvrir en iframe et appellera groupeCallback
* avec le groupe (quand il aura été sauvegardé si le user va au bout)
* @param {string} [baseId] L'id de la sésathèque
* @param {groupeCallback} groupeCallback
* @returns {string} L'url à ouvrir en iframe
*/
export function addGroupe (baseId, groupeCallback) {
const baseUrl = getBaseUrl(baseId)
const id = addCloser(baseUrl, groupeCallback)
return baseUrl + 'groupe/ajouter?layout=iframe&closerId=' + id
}
/**
* Ajoute des tokens d'authentification sur les sésathèques, qui seront envoyés dans le header authorization
* (sesalab-sso ne connait que des baseUrl, il met des authBundles en session coté sesalab
* et sesalab nous appelle avec s'il y en a)
* @param {object[]} authBundles tableau d'objets {baseUrl:{string}, token:{string}}
*/
export function addTokens (authBundles) {
try {
authBundles.forEach(function (authToken) {
const baseId = getBaseId(authToken.baseUrl)
addSesalabSsoToken(baseId, authToken.token)
})
} catch (error) {
console.error(error)
}
}
/**
* Clone un item (créera une ressource sur la sésathèque mentionnée)
* @param {string} baseId L'id de la sésathèque qui aura le clone (l'alias)
* @param {ClientItem} item L'item à cloner
* @param {itemCallback} next
*/
export function cloneItem (baseId, item, next) {
try {
log.debug(`cloneItem sur ${baseId}`, item)
if (!item.rid) throw Error('Item non clonable (sans rid)')
const aliasBaseUrl = getBaseUrl(baseId)
// on veut la baseId de destination d'un éventuel alias
const url = aliasBaseUrl + 'api/createAlias/' + item.rid
callApiUrl(url, wantDataOptions, function (error, data) {
if (error) return next(error)
else if (data.clone && data.clone.aliasOf) next(null, normalize(data.clone))
else next(Error(`Échec de la copie de l’item ${item.titre}`))
})
} catch (error) {
next(error)
}
}
/**
* Efface un item
* @param {ClientItem} item
* @param {simpleCallback} next
*/
export function deleteItem (item, next) {
log.debug('deleteItem', item)
try {
if (!item.$deleteUrlApi) return next(Error(`Information manquante pour supprimer « ${item.titre} » (pb de droits ?)`))
const options = {
method: 'delete',
isDataExpected: true
}
callApiUrl(item.$deleteUrlApi, options, function (error, data) {
if (!error && data.deleted) return next()
// si on est encore là y'a un truc louche…
const msg = `La suppression de l'item « ${item.titre} » a échouée`
if (error) error.message = `${msg} (${error.message})`
else error = Error(msg)
// @todo ajouter un appel busgnag ici avec error + qq infos
next(error)
})
} catch (error) {
next(error)
}
}
/**
* Retourne l'objet config (@see {@link ./config.js}
*/
export const getConfig = () => config
/**
* Retourne les enfants en liste d'items (pas forcément les petits enfants,
* il faudra rappeler cette fonction si enfants est une liste vide)
* @param {ClientItem} item
* @param {itemsCallback} next
*/
export function getEnfants (item, next) {
// si on les a déjà c'est plus simple…
if (item.enfants && item.enfants.length) return next(null, item.enfants)
if (!item.$dataUrl) return next(null, [])
callApiUrl(item.$dataUrl, wantDataOptions, function (error, arbre) {
if (error) return next(error)
if (arbre && arbre.type !== 'arbre') return next(Error('item avec $dataUrl qui ne pointe pas sur un arbre'))
if (arbre.enfants && arbre.enfants.length) next(null, arbre.enfants.map(normalize))
else next(null, [])
})
}
/**
* Récupère la liste des groupes dont on est admin
* @param {string} [baseId] L'id de la sésathèque
* @param {groupesCallback} next
*/
export function getGroupesAdmin (baseId, next) {
getGroupes(baseId, 'groupes/admin', 'groupesAdmin', next)
}
/**
* Récupère la liste des groupes dont on est membre
* @param {string} [baseId] L'id de la sésathèque
* @param {groupesCallback} next
*/
export function getGroupesMembre (baseId, next) {
getGroupes(baseId, 'groupes/membre', 'groupesMembre', next)
}
/**
* Récupère la liste des groupes suivis
* @param {string} [baseId] L'id de la sésathèque
* @param {groupesCallback} next
*/
export function getGroupesSuivis (baseId, next) {
getGroupes(baseId, 'groupes/suivis', 'groupesSuivis', next)
}
/**
* Retourne l'url de gestion des groupes (interface html)
* @param {string} baseId
*/
export function getGroupesUrl (baseId) {
const baseUrl = getBaseUrl(baseId)
return baseUrl + 'groupes/perso'
}
/**
* Retourne l'url d'accès au formulaire de recherche avancé'
* @param {string} baseId
*/
export function getSmartSearchUrl (baseId) {
return getBaseUrl(baseId) + 'autocomplete?layout=iframe'
}
/**
* Récupère un item sur une sésathèque
* @param {string} rid L'identifiant unique de la ressource (baseId/origine/idOrigine également autorisé ici)
* @param {boolean} [isPublic] passer true pour forcer public (sinon c'est privé si on a un token pour cette sésathèque)
* @param {itemCallback} next
*/
export function getItem (rid, isPublic, next) {
try {
if (typeof isPublic === 'function') {
next = isPublic
isPublic = undefined
}
if (!rid) return next(Error('getItem appelé sans rid'))
// on appelle pas getRidComponent car on veut pas de throw sur du baseId/origine/idOrigine
const [baseId, id] = getComponents(rid)
if (!exists(baseId)) throw Error(`paramètre rid incorrect (${rid} => sésathèque inconnue)`)
const baseUrl = getBaseUrl(baseId)
// init isPublic
if (typeof isPublic === 'function') {
next = isPublic
isPublic = !hasToken(baseId)
} else if (typeof isPublic === 'boolean') {
if (!isPublic && !hasToken(baseId)) {
console.error(`Pas de token dispo pour ${baseId}, appel public forcé`)
isPublic = true
}
} else {
isPublic = !hasToken(baseId)
}
// go
const url = `${baseUrl}api/${isPublic ? 'public' : 'ressource'}/${id}?format=ref`
callApiUrl(url, wantDataOptions, function (error, ref) {
if (error) return next(error)
next(null, normalize(ref))
})
} catch (error) {
next(error)
}
}
/**
* Récupère la liste des ressources publiques des formateurs donnés
* @param {string} baseId L'id de la sésathèque ajoutée au préalable
* @param {string[]} pids Une liste de pid (personneId, format authSourceBaseId/id)
* @param {Object} [options]
* @param {number} [options.skip]
* @param {number} [options.limit]
* @param {itemsByAuteurCallback} next appelé avec (error, itemList)
*/
export function getListeAuteurs (baseId, pids, options, next) {
if (typeof options === 'function') {
next = options
options = {}
}
let url
try {
if (!options.limit) options.limit = highLimit
url = getUrl(baseId, 'api/liste/auteurs', options)
url += '&pids=' + encodeURIComponent(pids.join(','))
} catch (error) {
return next(error)
}
callApiUrl(url, wantDataOptions, function (error, data) {
if (error) return next(error)
// on normalise les listes
Object.values(data).forEach((pidData) => {
if (pidData.liste) pidData.liste = pidData.liste.map(normalize)
})
next(null, data)
})
}
/**
* Récupère la liste des ressources d'un groupe
* @param {string} [baseId] L'id de la sésathèque
* @param {GroupItem|string} groupItem Le groupe dont on veut les ressources (ou son nom)
* @param {Object} [options]
* @param {number} [options.skip]
* @param {number} [options.limit]
* @param {itemsCallback} next
*/
export function getListeGroupe (baseId, group, options, next) {
if (typeof options === 'function') {
next = options
options = {}
}
try {
if (!options.limit) options.limit = highLimit
let url = getUrl(baseId, 'api/liste', options)
const groupName = typeof group === 'string' ? group : group.name
// y'a forcément du ?limit=xxx, on peut ajouter & directement
url += '&groupes=' + encodeURIComponent(groupName)
getListe(url, next)
} catch (error) {
next(error)
}
}
/**
* Récupère la liste des ressources perso du user courant
* @param {string} baseId L'id de la sésathèque ajoutée au préalable
* @param {Object} [options]
* @param {number} [options.skip]
* @param {number} [options.limit]
* @param {itemsCallback} next appelé avec (error, itemList)
*/
export function getListePerso (baseId, options, next) {
if (typeof options === 'function') {
next = options
options = {}
}
try {
if (!options.limit) options.limit = highLimit
const url = getUrl(baseId, 'api/liste/perso', options)
getListe(url, next)
} catch (error) {
next(error)
}
}
/**
* Récupère une ressource complète sur une sésathèque
* @param {string} rid L'identifiant unique de la ressource (baseId/origine/idOrigine également autorisé ici)
* @param {ressourceCallback} next
*/
export function getRessource (rid, next) {
// on autorise aussi les ids avec du / pour imposer une autre baseUrl
const [baseId, id] = getComponents(rid)
if (baseId && exists(baseId)) {
// si on a pas de token pas la peine de tenter la voie authentifiée
const prefix = hasToken(baseId) ? 'ressource/' : 'public/'
log.debug(`préfixe ${prefix} avec ${hasToken(baseId)}`)
callApi(baseId, prefix + id, (error, data) => {
if (error) return next(error)
next(null, new Ressource(data))
})
} else {
next(Error(`rid invalide (${rid} => sésathèque inconnue)`))
}
}
/**
* Crée un nouveau groupe
* @param {string} [baseId] L'id de la sésathèque
* @param {string} groupName Le nom du groupe
* @param {feedbackCallback} next
*/
export function groupCreate (baseId, groupName, next) {
if (typeof groupName !== 'string') return next(Error('Nom de groupe invalide'), 'error')
const apiPath = 'groupe/ajouter/' + encodeURIComponent(groupName)
const successMessage = 'Le groupe ' + groupName + ' a été créé'
const errorMessage = 'La création du groupe ' + groupName + ' a échoué'
action(baseId, apiPath, successMessage, errorMessage, next)
}
/**
* Stoppe le suivi d'un groupe (on en sera plus follower)
* @param {string} [baseId]
* @param {GroupItem} group
* @param {feedbackCallback} next
*/
export function groupIgnore (baseId, group, next) {
if (group && group.$urlIgnore && group.name) {
const apiPath = 'groupe/ignorer/' + encodeURIComponent(group.name)
const successMessage = 'Le groupe ' + group.name + ' a été créé'
const errorMessage = 'La création du groupe ' + group.name + ' a échoué'
action(baseId, apiPath, successMessage, errorMessage, next)
callApi(baseId, 'groupe/ignorer', function (error, response) {
const errorMessage = `Une erreur est survenue, vous suivez probablement encore les ressources du groupe ${group.name}`
if (error) next(errorMessage + ` (${error.toString()})`, 'error')
else if (response.success) next('Vous ne suivez plus les ressources du groupe ' + group.name)
else next(errorMessage, 'error')
})
} else {
if (group) log.error('Ce groupe n’aurait pas dû pouvoir appeler groupIgnore', group)
else log.error(Error('arguments invalides'), arguments)
next('Vous ne suivez pas les ressources de ce groupe', 'error')
}
}
/**
* Quitte un groupe (on en sera plus membre ni follower)
* @param {string} [baseId]
* @param {GroupItem} group
* @param {feedbackCallback} next
*/
export function groupQuit (baseId, group, next) {
if (group && group.$urlQuit) {
callApi(baseId, 'groupe/quitter', function (response) {
if (response.success) next('Vous avez quitté le groupe ' + group.name)
else if (response.error) next(response.error, 'error')
else next('Une erreur est survenue, vous êtes peut-être toujours membre du groupe' + group.name, 'error')
})
} else {
if (group) console.error('Ce groupe n’aurait pas dû pouvoir appeler groupQuit', group)
else console.error('arguments invalides')
next('Vous n’êtes pas membre de ce groupe et ne pouvez le quitter', 'error')
}
}
/**
* Retourne le code niveau probable d'après un nom de classe
* @type {function}
* @param {string} classe
* @returns {string} Le code du niveau, undefined si on a rien deviné
*/
export const guessNiveau = config.guessNiveau
/**
* Pour modifier un item, retourne d'abord une url à ouvrir en iframe à urlCallback
* puis un item sauvegardé à itemCallback (quand la ressource aura été sauvegardée si le user va au bout)
* @param {ClientItem} item
* @param {itemCallback} itemCallback
* @param {urlCallback} urlCallback
*/
export function modifyItem (item, itemCallback, urlCallback) {
if (item.$editUrl) {
const baseUrl = extractBaseUrl(item.$editUrl)
const id = addCloser(baseUrl, itemCallback)
urlCallback(null, item.$editUrl + '?layout=iframe&closerId=' + id)
} else {
console.error('modifyItem avec item sans $editUrl', item)
urlCallback(Error('Cet item ne peut être modifié'))
}
}
/**
* Retourne un item de sesatheque normalisé
* @param {Ref|Ressource} ressource
* @returns {ClientItem} Toujours un ClientItem, éventuellement un fake {type:error, titre: errorMessage} en cas de pb
*/
export function normalize (ressource) {
try {
return new ClientItem(ressource)
} catch (error) {
log.error(error, ressource)
return {
titre: error.toString(),
type: 'error'
}
}
}
/**
* Notifie une erreur à une sésathèque, à utiliser pour les pbs de datas
* garder bugsnag pour les erreurs imprévues du code
* @param baseId
* @param message
* @param obj
*/
export function notifyError (baseId, message, obj) {
if (!exists(baseId)) return console.error(Error(baseId + ' n’est pas une base de sésathèque connue'))
const notif = { error: message }
if (obj) notif.detail = obj
const url = getBaseUrl(baseId) + 'api/notifyError'
const options = {
method: 'post',
body: notif
}
callApiUrl(url, options, log.ifError)
}
/**
* Exporte une séquence modèle d'un sesalab comme ressource sur une sesatheque
* @param {string} baseId
* @param {object} sequenceModele un objet sequence de sesalab, il doit avoir nom et oid, éventuellement des groupes et une propriété public (booléen à true si "tout le monde" est coché pour le partage)
* @param {itemCallback} next sera rappelé avec une erreur ou un item valide
*/
export function saveSequenceModele (baseId, sequenceModele, next) {
log.debug('saveSequenceModele sur ' + baseId, sequenceModele)
try {
if (!sequenceModele) throw Error('Modèle de séquence manquant')
if (!sequenceModele.nom) throw Error('Modèle de séquence sans titre')
if (!sequenceModele.oid) throw Error('Modèle de séquence sans identifiant (oid)')
const ressource = {
titre: sequenceModele.nom,
type: 'sequenceModele',
origine: origine,
idOrigine: sequenceModele.oid,
publie: true,
parametres: filters.object(sequenceModele, /^(\$|_)/)
}
if (Array.isArray(sequenceModele.groupes)) ressource.groupes = sequenceModele.groupes
if (sequenceModele.public) ressource.restriction = config.constantes.restriction.aucune
else if (sequenceModele.groupes) ressource.restriction = config.constantes.restriction.groupe
else ressource.restriction = config.constantes.restriction.prive
saveRessource(baseId, ressource, next)
} catch (error) {
log.error(error)
next(error)
}
}
/**
* Exporte une série d'un sesalab comme ressource sur une sesatheque
* @param [baseId] L'id de la Sésathèque
* @param {{titre:string, parametres:object}} data (la propriété parametres doit avoir une propriété serie)
* @param {itemCallback} next sera rappelé avec une erreur ou un item valide
*/
export function saveSerie (baseId, data, next) {
log.debug('saveSerie sur ' + baseId, data)
try {
if (!data.titre) throw Error('Série sans titre')
if (!data.parametres) throw Error('pas de paramètres')
if (!data.parametres.serie) throw Error('pas de série')
var ressource = {
origine: origine,
idOrigine: data.idOrigine || uuid(),
titre: data.titre,
type: 'serie',
publie: true,
restriction: config.constantes.restriction.prive,
parametres: data.parametres
}
if (ressource.parametres.serie && ressource.parametres.serie.length) {
// faut virer les propriétés $behavior qui plantent le JSON.stringify
ressource.parametres.serie = ressource.parametres.serie.map((exo) => filters.object(exo, /^(\$|_)/))
}
saveRessource(baseId, ressource, next)
} catch (error) {
log.error(error)
next(error)
}
}
/**
* Un objet dont les clés sont des propriétés de ressource et chaque valeur un array avec les valeurs à filtrer pour cette propriété
* Par ex {categories: [1, 2], niveaux: ['6']}
* @typedef searchQueryFilters
* @type Object
*/
/**
* @callback searchQueryFiltersCallback
* @param {Error} [error]
* @param {searchQueryFilters} searchQueryFilters
*/
/**
* Retourne une liste de filtres de recherche qui matchent le pattern donné
* @param {string} baseId Identifiant de la sésathèque
* @param {string} search Pattern de recherche
* @param {searchQueryFiltersCallback} next Callback
*/
export function autocomplete (baseId, search, next) {
const url = getBaseUrl(baseId) + `api/autocomplete/${search}`
callApiUrl(url, wantDataOptions, function (error, data) {
if (error) return next(error)
if (!Array.isArray(data.filters)) return next(Error(`Réponse malformée (filters n’est pas un tableau)`))
const { filters } = data
// Transforme les résultats (Array<{index, value}>) au format {indexName: [values]}
const queryFilters = {}
filters.forEach(({ index, value }) => {
if (!index) return log.error(`Réponse invalide de ${url} (index vide sur un item)`)
if (!['number', 'string'].includes(typeof value)) return log.error(`Réponse invalide de ${url} (value ≠ string|number sur un item ${typeof value})`)
if (queryFilters[index] === undefined) queryFilters[index] = []
// un pattern ne peut pas renvoyer deux fois la même valeur pour le même index, pas besoin de dédupliquer
// if (!queryFilters[index].includes(value)) queryFilters[index].push(value)
// et si jamais ça arrivait ce serait pas un drame car ça donnerait une requête contenant des valeur en double qui remontera la même chose
queryFilters[index].push(value)
})
next(null, queryFilters)
})
}
/**
* Récupère des résultats de recherche
* @param {string} baseId Identifiant de la sésathèque
* @param {searchQueryFilters} searchQueryFilters Object contenant les paramètres de recherche au format {indexName: [...valeurs]}
* @param {function} next Callback
*/
export function search (baseId, searchQueryFilters, next) {
const queryStringParams = {
format: 'light',
...searchQueryFilters
}
const url = getBaseUrl(baseId) + 'api/liste?' + queryString.stringify(queryStringParams)
callApiUrl(url, wantDataOptions, function (error, data) {
if (error) return next(error)
if (!Array.isArray(data.liste)) return next(Error(`Réponse malformée (liste n’est pas un tableau mais ${typeof data.liste})`))
next(null, data.liste)
})
}
export function setOrigine (newOrigine) {
if (typeof newOrigine !== 'string') throw new TypeError('origine doit être une string')
origine = newOrigine
}
/**
* Modifie le timeout des prochains appels
* @param {number} sTimeout Nouveau timeout en secondes
*/
export function setTimeout (sTimeout) {
timeout = Number(sTimeout) * 1000
if (timeout < 500) timeout = 500
}
export default {
// les réexports
addSesatheque,
addSesatheques,
exists,
reBaseUrl,
ClientItem,
// et tous nos exports nommés
addItem,
addGroupe,
addTokens,
cloneItem,
deleteItem,
getConfig,
getEnfants,
getGroupesAdmin,
getGroupesMembre,
getGroupesSuivis,
getGroupesUrl,
getSmartSearchUrl,
getItem,
getListeAuteurs,
getListeGroupe,
getListePerso,
getRessource,
groupCreate,
groupIgnore,
groupQuit,
guessNiveau,
modifyItem,
normalize,
notifyError,
saveSequenceModele,
saveSerie,
autocomplete,
search,
setOrigine,
setTimeout
}
/**
* Un groupe remonté par getGroupesSuivis
* @typedef GroupeItem
* @property {string} name
* @property {boolean} $publish Si existe et true, l'utilisateur peut publier dans ce groupe (lui afficher une case à cocher dans le partage de séquence)
* @property {string} [$urlAdmin] Si existe l'utilisateur peut administrer ce groupe (on peut lui ouvrir une iframe sur cette url)
* @property {string} [$canQuit] Si existe on peut ajouter un lien "quitter ce groupe" qui appellera la fct groupQuit
* @property {string} [$canIgnore] Si existe on peut ajouter un lien "ne plus suivre ce groupe" qui appellera la fct groupIgnore
*/
/**
* @callback itemCallback
* @param {Error} [error]
* @param {ClientItem} [item]
*/
/**
* @callback feedbackCallback
* @param {string} message Le message à afficher en feedback
* @param {string} type Le type de message (info|warning|error)
*/
/**
* @callback itemsCallback
* @param {Error} [error]
* @param {ClientItem[]} [items]
*/
/**
* @callback itemsByAuteurCallback
* @param {Error} [error]
* @param {Object} [itemsByAuteur] un objet dont les clés sont les pids des auteurs demandés
* @param {ClientItem[]} [itemsByAuteur.xxx]
*/
/**
* @callback apiResponseCallback
* @param {Error} error
* @param {object} response La réponse de l'api si y'a pas d'erreur (soit la string "OK" soit les data de la réponse, toujours objet)
*/
/**
* @callback refCallback
* @param {Error} error
* @param {Ref} ref
*/
/**
* @callback ressourceCallback
* @param {Error} error
* @param {Ressource} ressource
*/
/**
* @callback simpleCallback
* @param {Error} [error]
*/
/**
* @callback urlCallback
* @param {Error} [error]
* @param {string} [url] Une url absolue
*/
/**
* @callback itemsCallback
* @param {Error} [error]
* @param {ClientItem[]} [items]
*/
/**
* @callback groupesCallback
* @param {Error} [error]
* @param {GroupeItem[]} groupes
*/
/**
* @callback groupeCallback
* @param {Error} [error]
* @param {GroupeItem} groupes
*/