'use strict'
const flow = require('an-flow')
const uuid = require('an-uuid')
const elementtree = require('elementtree')
const _ = require('lodash')
const request = require('request')
const { parse } = require('sesajstools')
const { exists, getBaseIdFromRid } = require('sesatheque-client/dist/server/sesatheques')
const Ref = require('../../constructors/Ref')
const { ensure, isEntity } = require('../lib/tools')
const { getRidEnfants } = require('../lib/ressource')
const config = require('./config')
const appConfig = require('../config')
const j3pGraphe2json = require('../../../tasks/modules/j3pGraphe2json')
const myBaseId = appConfig.application.baseId
// et des petites fonctions utiles
const prependMyBaseId = (oid) => myBaseId + '/' + oid
const getRealRid = (ressource) => ressource.aliasOf || ressource.rid
module.exports = function (ressourceComponent) {
ressourceComponent.service('$ressourceRepository', function (EntityRessource, EntityArchive, EntityExternalRef, $ressourceRemote, $ressourceControl, $cacheRessource, $cache, $routes) {
// on applique toujours un limit
const { listeMax, listeNbDefault } = config.limites
if (!listeMax) throw new Error('ressource.limites.listeMax manquant en configuration')
/**
* Applique checkRelations et dropAliasEnfants avant d'appeler next
* @private
* @param {Ressource} ressource
* @param {ressourceCallback} next
*/
function beforeSave (ressource, next) {
// si ressource est un alias, ça va virer les enfants, c'est ce qu'on veut
if (ressource.enfants && ressource.enfants.length) dropAliasEnfants(ressource)
checkRelations(ressource, next)
}
/**
* Nettoie les relations (helper de save, en complément de beforeStore)
* @private
* @param {Ressource} ressource
* @param {function} next
*/
function checkRelations (ressource, next) {
if (!ressource.relations || !ressource.relations.length) return next(null, ressource)
const id = ressource.oid || ressource.rid || ressource.titre
const cleanRelations = ressource.relations
// on veut un tableau de 2 éléments dont le 1er est une relation connue
.filter(relation => Array.isArray(relation) && relation.length === 2 && config.listes.relations[relation[0]])
// cast des 2 éléments
.map(relation => [Number(relation[0]), String(relation[1])])
if (cleanRelations.length < ressource.relations) log.dataError(`relations fournies invalides pour ${ressource.oid}`, ressource.relations)
// le format est bon (typeRel connu), reste à voir si on a des cibles sous la forme origine/idOrigine
// et ajouter éventuellement baseId pour avoir un rid valide
if (cleanRelations.length) {
flow(cleanRelations).parEach(function (relation) {
const nextRelation = this
const [relId, relTarget] = relation
const pos = relTarget.indexOf('/')
if (pos === -1) {
// on suppose que c'est un oid
nextRelation(null, [relId, prependMyBaseId(relTarget)])
} else {
// au moins un slash
const debut = relTarget.substr(0, pos)
const fin = relTarget.substr(pos + 1)
// si c'est chez nous on vérifie
if (debut === myBaseId) {
load(fin, function (error, ressource) {
if (error) return nextRelation(error)
if (ressource) {
nextRelation(null, [relId, getRealRid(ressource)])
} else {
log.dataError(`${fin} n’existe pas sur cette sesathèque (mentionné comme relation de ${id}`)
nextRelation()
}
})
// ailleurs, on fait confiance et une tâche en cli vérifiera de tps en tps
} else if (exists(debut)) {
nextRelation(null, relation)
// debut devrait être une origine, on vérifie que la ressource existe
} else {
loadByOrigin(debut, fin, function (error, ressource) {
if (error || !ressource) {
if (error) log.error(error)
else log.dataError(`${relTarget} n’existe pas sur cette sesathèque (mentionné comme relation de ${id}`)
return nextRelation()
}
nextRelation(null, [relId, getRealRid(ressource)])
})
}
}
}).seq(function (relations) {
// reste à virer les éventuels undefined et les doublons
// pour les doublons, on utilise un accumulateur avec des clés plutôt qu'un set/map
// (test sur booléen plus pertinent que set puis Array.from)
const acc = {}
const checkedRelations = relations.filter(relation => {
if (!relation) return false
const [typeRel, targetRid] = relation
const key = `${typeRel}${targetRid}`
if (acc[key]) return false
acc[key] = true
return true
})
const perte = ressource.relations.length - checkedRelations.length
if (perte) {
const s = perte === 1 ? '' : 's'
log.dataError(`Il y avait ${perte} relation${s} en double (ou invalide${s}) dans ${id}`, relations)
ressource.relations = checkedRelations
}
next(null, ressource)
}).catch(next)
} else {
ressource.relations = []
next(null, ressource)
}
}
/**
* Modifie ref en virant récursivement les enfants de tout item qui serait un alias
* (pour garder l'aspect dynamique)
* @private
* @param {Ref|Ressource} ref
*/
function dropAliasEnfants (ref) {
if (ref.enfants) {
if (ref.aliasOf) delete ref.enfants
else if (ref.enfants.length) ref.enfants.forEach(dropAliasEnfants)
}
}
/**
* Retourne la requête lassi, préparée d'après searchQuery
* @private
* @param {searchQuery} searchQuery Un objet avec les match à faire en propriété et les valeurs à matcher en value (toujours Array)
* @param {getListeFilters} [options.filters] tableau de filtres
* @return {EntityQuery}
*/
function getLassiQuery (searchQuery) {
const lassiQuery = EntityRessource.match() // sans argument ça retourne une EntityQuery vierge
Object.entries(searchQuery).forEach(([prop, values]) => {
if (!Array.isArray(values)) throw new Error(`values invalides (pas un tableau) pour ${prop}`)
if (values.length) {
if (prop === 'fulltext') {
lassiQuery.textSearch(`"${values.join('" "')}"`)
} else if (values.length > 1) {
lassiQuery.match(prop).in(values)
} else {
const value = values[0]
if (typeof value === 'string' && value.includes('%')) {
lassiQuery.match(prop).like(value)
} else {
lassiQuery.match(prop).equals(value)
}
}
} else {
// pas de valeur, on fait juste un match sur la prop, sauf fulltext qui n'y a pas droit
if (prop === 'fulltext') throw new Error('searchQuery avec fulltext sans values')
lassiQuery.match(prop)
}
})
return lassiQuery
}
/**
* Met à jour cette ref dans tous les arbres des autres sesathèques qui la contiennent,
* @private
* @param {Ref} ref
*/
function updateParentsExternes (ref) {
// et on met à jour sur les autres sesathèques qui pourraient avoir mis cet enfant dans un arbre
const stCalled = new Set()
const rid = ref.aliasOf
flow().seq(function () {
EntityExternalRef.match('rid').equals(rid).grab(this)
}).seqEach(function (extRef) {
const { baseId } = extRef
if (stCalled.has(baseId)) {
log.dataError(new Error(`EntityExternalRef ${extRef.oid} en double pour ${baseId} et ${rid}, on la supprime`))
extRef.delete(this)
return
}
stCalled.add(baseId)
$ressourceRemote.externalUpdate(baseId, ref, this)
})
// faut vider la pile sinon an-flow râle parce qu'on appelle un seq qui n'existe pas avec des data
.empty()
.catch(log.error)
}
/**
* Purge les urls publiques de la ressource sur varnish (si varnish est dans la conf, ne fait rien sinon)
* (rend la main avant les réponses mais après avoir lancé les requêtes)
* @param {Ressource|string} ressource ou son oid
*/
function purgeVarnish (ressource) {
if (!appConfig.varnish) return
const myBaseUrl = appConfig.application.baseUrl
// on ne purge que les ressources publiques (les autres ne sont pas en cache)
if (ressource.publie && ressource.restriction === config.constantes.restriction.aucune) {
// log.debug(`purge varnish de ${ressource.oid} en utilisant ${myBaseUrl}`)
;
[
$routes.getAbs('api', ressource),
$routes.getAbs('display', ressource),
$routes.getAbs('describe', ressource),
$routes.getAbs('preview', ressource)
].forEach(function (url) {
request({
method: 'PURGE',
url: myBaseUrl + url.substr(1) // pour pas avoir de double slash
}, function (error, response) {
log.debug('purge ' + url + ' ' + (response && response.statusCode))
if (error) {
log.error('purge KO pour ' + url, error)
} else if (!response || response.statusCode !== 200) {
log.error('purge KO (!200) pour ' + url, response)
log.error('avec le body', arguments[2])
}
})
})
}
}
/**
* Helper du save, pour
* - incrémenter le n° de version si la ressource a une propriété versionNeedIncrement
* ou si une des propriétés listées dans config.versionTriggers a changée de valeur
* - incrémenter aussi inc si une des propriétés listées dans config.suffixTriggers a changée de valeur
* - lancer en tâche de fond, si besoin, l'update des arbres qui utilisent cette ressource, ici et sur toutes
* les sésathèques qui référencent cette ressource via un ExternalRef
* - mettre à jour nos listener si les externalRef de cette ressource ont changé
* @private
* @param {EntityRessource} ressource
* @param {Function} next
*/
function checkAgainstPrevious (ressource, next) {
// on met à jour les parents si besoin en tâche de fond
function updateParents (ressource) {
const ref = new Ref(ressource)
updateParent(ref)
updateParentsExternes(ref)
}
// normal au create
if (!ressource.oid) return next(null, ressource)
if (!ressource.$original) {
// ça devrait pas se produire, on laisse passer mais on le signale
log.dataError('ressource avec oid sans $original au save', ressource)
return next(null, ressource)
}
const original = parse(ressource.$original)
// incrément de version ?
// pour la comparaison, deux objets avec la même définition littérale sont vus != en js
// on utilise https://lodash.com/docs#isEqual
const versionNeedIncrement = ressource.versionNeedIncrement || config.versionTriggers.some((prop) => !_.isEqual(ressource[prop], original[prop]))
// inc
const incNeedIncrement = versionNeedIncrement || config.incTrigger.some(prop => !_.isEqual(ressource[prop], original[prop]))
log.debug(`dans checkAgainstPrevious pour ${ressource.oid} on a incNeedIncrement ${incNeedIncrement} et versionNeedIncrement ${versionNeedIncrement}`)
ressource.inc = original.inc
// version
ressource.version = original.version
if (versionNeedIncrement) {
// faut register / unregister si la liste de nos éventuels enfants externes a changé (si c'est le cas la version a forcément changé)
checkRegisterListener(ressource, original)
// on archive et récupère l'oid de l'archive pour la mettre sur la ressource (et incrémenter après archivage)
log.debug('on va archiver', ressource)
archive(original, function (error, archive) {
if (error) return next(error)
log.debug('ressource archivée', archive)
ressource.version++
ressource.inc++
next(null, ressource)
updateParents(ressource)
})
} else {
if (incNeedIncrement) ressource.inc++
next(null, ressource)
updateParents(ressource)
}
}
/**
* Enregistre ou supprime des listeners pour nos enfants externes s'ils ont changé
* @param {Ressource} ressource
* @param {Ressource} ressourceBdd
*/
function checkRegisterListener (ressource, ressourceBdd) {
if ((ressource.enfants && ressource.enfants.length) || (ressourceBdd.enfants && ressourceBdd.enfants)) {
// faut la liste des rid externes qui ont changés
const oldRids = getRidEnfants(ressourceBdd)
const newRids = getRidEnfants(ressource)
const externalRidsAdded = newRids.filter(rid => !oldRids.includes(rid) && getBaseIdFromRid(rid) !== myBaseId)
// on pourrait facilement trouver les rids qui ont été virés, mais on peut pas faire de unregister
// sans vérifier que personne d'autre ne les utilise, on laisse tomber et ça sera viré au 1er update
// si personne s'en sert
appConfig.sesatheques.forEach(sesatheque => {
const rids = externalRidsAdded.filter(rid => getBaseIdFromRid(rid) === sesatheque.baseId)
if (rids.length) $ressourceRemote.register(rids, error => { if (error) log.error(error) })
})
}
}
/**
* Met en cache et fait suivre
* Si on trouve un parametres.xml, on le jsonify
* @private
* @param error
* @param ressources ressource ou tableau de ressources (ou rien, sera passé à next tel quel)
* @param next appelé avec (error, ressources)
* @throws {Error} Si ressources est défini mais n'est pas une ressource ou un tableau de ressources
*/
function cacheAndNext (error, ressources, next) {
/**
* Helper qui process une ressource et la met en cache (et rend la main sans attendre le résultat)
* @private
* @param ressource
*/
function processOne (ressource) {
if (!ressource || !ressource.oid) throw new Error('Paramètre invalide (ressource attendue)')
// pour ec2 on regarde si on a un xml et rien d'autre pour le mettre directement en parametres
// (car c'est pas du xml mais du json)
if (ressource.type === 'ec2' && ressource.parametres && ressource.parametres.xml) {
convertXmlEc2(ressource)
} else if (ressource.type === 'j3p' && ressource.parametres && ressource.parametres.xml) {
convertXmlJ3p(ressource)
}
$cacheRessource.set(ressource)
}
try {
if (error) {
log.error(error)
return next(new Error('Problème d’accès à la base de données'))
}
if (Array.isArray(ressources)) ressources.forEach(processOne)
else if (ressources) processOne(ressources)
next(null, ressources)
} catch (error) {
next(error)
}
}
/**
* Converti un xml qui trainerait en parametres en json pour les ec2
* @param ressource
*/
function convertXmlEc2 (ressource) {
const config = elementtree.parse(ressource.parametres.xml)
const params = {}
/*
{ _root:
{ _id: 0,
tag: 'config',
attrib: {},
text: '\r\n',
tail: null,
_children: [
[ { _id: 1,
tag: 'swf',
attrib: {},
text: 'calcul-differe-3.swf',
tail: '\n',
_children: [] },
{ _id: 2,
tag: 'json',
attrib: {},
text: 'default',
tail: '\r\n',
_children: [] } ]
]
}
}
*/
if (config._root && config._root.tag === 'config' && config._root._children) {
config._root._children.forEach(function (child) {
if (child.tag) {
params[child.tag] = child.text
}
})
ressource.parametres = params
// on enregistre la ressource modifiée en async
save(ressource, error => error && console.error(error))
}
log.debug('convertXmlEc2', params)
}
/**
* Modifie les parametres de ressource pour remplacer un éventuel xml par un graphe
* @param {Ressource} ressource
*/
function convertXmlJ3p (ressource) {
if (ressource.parametres && ressource.parametres.xml) {
const string = j3pGraphe2json(ressource.parametres.xml)
try {
const graphe = JSON.parse(string)
ressource.parametres = {
g: graphe
}
// on enregistre pour pas revenir ici la prochaine fois
save(ressource, error => error && console.error(error))
} catch (error) {
log.error('plantage dans la conversion du xml de la ressource j3p')
if (!ressource.$errors) ressource.$errors = []
ressource.$errors.push('la propriété xml des parametres ne contient pas de graphe valide')
}
}
}
/**
* Les méthodes du service exportées
*/
/**
* Enregistre la ressource en archive et appelle next avec, mais ne modifie pas la ressource
* @memberOf $ressourceRepository
* @param {EntityRessource} ressource
* @param next appelé avec (error, archive)
*/
function archive (ressource, next) {
if (!ressource.oid) throw Error('Impossible d’archiver une ressource qui n’existe pas encore')
const data = Object.assign({}, ressource, { oid: undefined })
EntityArchive.create(data).store(next)
}
/**
* Efface une ressource (et ses index)
* @memberOf $ressourceRepository
* @param {EntityRessource|string} ressource (ou son oid)
* @param {errorCallback} next
* @returns {undefined}
*/
function remove (ressource, next) {
const ressourceOid = typeof ressource === 'string' ? ressource : (ressource && ressource.oid)
if (!ressourceOid) return next(new Error('remove appelé sans ressource'))
log.debug(`La ressource ${ressourceOid} va être effacée`)
// on vire du cache de toute façon
$cacheRessource.delete(ressourceOid)
EntityRessource.match('oid').equals(ressourceOid).purge(function (error, nb) {
if (error) return next(error)
if (nb > 1) next(new Error(`L’effacement de la ressource ${ressourceOid} a provoqué ${nb} suppressions`))
if (nb === 1) log.debug(`La ressource ${ressourceOid} a été effacée`)
else log.debug(`La ressource ${ressourceOid} n’existait pas`)
purgeVarnish(ressource)
next()
})
}
/**
* Supprime un groupe de toutes les ressources qui le contiennent (groupes et groupesAuteurs)
* @param {string} nom
* @param {errorCallback} next
*/
function removeGroup (nom, next) {
// Supprime dans groupes (+groupesAuteurs pour les ressources concernées)
const deleteInGroupes = (skip) => {
flow().seq(function () {
EntityRessource.match('groupes').equals(nom).grab(this)
}).seqEach(function (ressource) {
ressource.groupes = ressource.groupes.filter(notMatch)
if (ressource.groupesAuteurs.length) ressource.groupesAuteurs = ressource.groupesAuteurs.filter(notMatch)
save(ressource, this)
}).seq(function (ressources) {
if (ressources.length < limit) return deleteInGroupesAuteurs(0)
deleteInGroupes(skip + limit)
}).catch(next)
}
// Supprime dans groupesAuteurs pour celles qui restent
const deleteInGroupesAuteurs = (skip) => {
flow().seq(function () {
EntityRessource.match('groupesAuteurs').equals(nom).grab(this)
}).seqEach(function (ressource) {
ressource.groupesAuteurs = ressource.groupesAuteurs.filter(notMatch)
save(ressource, this)
}).seq(function (ressources) {
if (ressources.length < limit) return next()
deleteInGroupesAuteurs(skip + limit)
}).catch(next)
}
const limit = 100
const notMatch = (groupeNom) => groupeNom !== nom
deleteInGroupes(0)
}
/**
* Récupère un liste de ressource d'après critères
* @param {searchQuery} searchQuery Les critères de tri
* @param {searchQueryOptions} queryOptions Les options (skip & limit + orderBy éventuel)
* @param {ressourcesCallback} next appelée avec (error, ressources)
*/
function grabSearch (searchQuery, queryOptions, next) {
try {
if (!queryOptions) queryOptions = {}
const lassiQuery = getLassiQuery(searchQuery)
const sort = (orderBy) => {
const [key, order] = orderBy
if (order === 'desc') lassiQuery.sort(key, 'desc')
else lassiQuery.sort(key)
}
// orderBy
if (Array.isArray(queryOptions.orderBy)) {
// un cas limite, un tableau à 2 elts dont le 2e est 'desc'
if (queryOptions.orderBy.length === 2 && ['asc', 'desc'].includes(queryOptions.orderBy[1])) {
sort(queryOptions.orderBy)
} else {
queryOptions.orderBy.forEach(orderBy => {
if (typeof orderBy === 'string') {
lassiQuery.sort(orderBy)
} else if (Array.isArray(orderBy)) {
sort(orderBy)
}
})
}
}
// limit & skip
let { limit, skip } = queryOptions
const wantedLimit = ensure(limit, 'integer', listeNbDefault)
if (wantedLimit > 0 && wantedLimit <= listeMax) {
limit = wantedLimit
} else {
// y'a un pb
if (wantedLimit > listeMax) {
log.error(new Error(`limite ${wantedLimit} demandée trop haute, ramenée à ${listeMax}`))
limit = listeMax
} else {
log.error(new Error(`limite ${wantedLimit} invalide, mise à ${listeNbDefault}`))
limit = listeNbDefault
}
}
skip = ensure(skip, 'integer', 0)
flow().seq(function () {
lassiQuery.grab({ limit, skip }, this)
}).seq(function (ressources) {
if (ressources.length) cacheAndNext(null, ressources, next)
else next(null, [])
}).catch(next)
} catch (error) {
next(error)
}
} // grabSearch
/**
* Compte le nb de ressources d'après les critères de recherche
* @param {searchQuery} searchQuery Les critères de tri
* @param {function} next appelée avec (error, total)
*/
function grabSearchCount (searchQuery, next) {
try {
const lassiQuery = getLassiQuery(searchQuery)
lassiQuery.count(next)
} catch (error) {
next(error)
}
}
/**
* Récupère une ressource et la passe à next (ressource undefined si elle n’existe pas)
* @memberOf $ressourceRepository
* @param {number|String} oid L'identifiant de la ressource (on accepte oid ou string origine/idOrigine)
* @param {ressourceCallback} next appelée avec une EntityRessource
* @returns {undefined}
*/
function load (oid, next) {
if (typeof oid !== 'string') throw Error(`load veut un id en string, pas ${typeof oid}`)
if (oid.includes('/')) {
const [origin, idOrigin, bug] = oid.split('/')
if (bug) return next(Error('identifiant invalide : ' + oid))
if (origin === 'cle') return loadByCle(idOrigin, next)
return loadByOrigin(origin, idOrigin, next)
}
// sinon c'est un oid
$cacheRessource.get(oid, function (error, ressourceCached) {
if (error) return next(error)
if (ressourceCached) return next(null, ressourceCached)
EntityRessource.match('oid').equals(oid).grabOne(function (error, ressource) {
cacheAndNext(error, ressource, next)
})
})
}
/**
* Récupère une ressource d'un auteur d'après son aliasOf (pour voir si cette personne
* a déjà un alias qui pointe sur aliasOf)
* @memberOf $ressourceRepository
* @param {string} aliasOf
* @param {string} rid
* @param {ressourcesCallback} next appelée avec une EntityRessource
*/
function loadByAliasAndPid (aliasOf, pid, next) {
EntityRessource
.match('aliasOf').equals(aliasOf)
.match('auteurs').equals(pid)
.grabOne(next)
} // loadByAliasAndPid
/**
* Récupère une ressource d'après sa cle et la passe à next
* @memberOf $ressourceRepository
* @param {string} cle
* @param {ressourceCallback} next appelée avec une EntityRessource
*/
function loadByCle (cle, next) {
if (!cle) return next(new Error('Clé manquante, impossible de charger la ressource'))
$cacheRessource.getByOrigine('cle', cle, function (error, ressourceCached) {
if (error) return next(error)
if (ressourceCached) return next(null, ressourceCached)
EntityRessource
.match('cle').equals(cle)
.grabOne(function (error, ressource) {
cacheAndNext(error, ressource, next)
})
})
} // loadByCle
/**
* Récupère une ressource d'après son idOrigine (ou son rid, dans ce cas origine est notre baseId) et la passe à next
* @memberOf $ressourceRepository
* @param {string} origine (ou "cle" avec idOrigine qui est la clé, ou la baseId courante avec idOrigine qui est l'oid)
* @param {string} idOrigine
* @param {ressourceCallback} next appelée avec une EntityRessource
*/
function loadByOrigin (origine, idOrigine, next) {
if (origine && idOrigine) {
// on est appelé par les controleurs sur les urls xxx/:origine/:idOrigine
// mais le client de l'api peut passer un rid ou une clé,
// on le gère ici plutôt que de mettre des if dans chaque contrôleur
if (origine === 'cle') {
return loadByCle(idOrigine, next)
}
if (origine === myBaseId) {
return load(idOrigine, next)
}
// c'est un vraie origine
$cacheRessource.getByOrigine(origine, idOrigine, function (error, ressourceCached) {
if (error) return next(error)
if (ressourceCached) return next(null, ressourceCached)
EntityRessource
.match('origine').equals(origine)
.match('idOrigine').equals(idOrigine)
.grabOne(function (error, ressource) {
cacheAndNext(error, ressource, next)
})
})
} else {
return next(new Error('Origine ou idOrigine manquant, impossible de charger la ressource'))
}
} // loadByOrigin
/**
* Met en cache la ressource et le user pour modification ultérieure
* @param {number} oid
* @param {function} next
*/
function saveDeferred (oid, next) {
const token = uuid()
// on met 10h en cache, vu le peu de data c'est pas un souci
$cache.set('defer_' + token, { oid: oid, action: 'saveRessource' }, 36000, function (error) {
if (error) next(error)
else next(null, token)
})
}
/**
* Récupère oid et user d'après le token
* @param token
* @param next
*/
function getDeferred (token, next) {
/* $cache.get('defer_' + token, function (error, data) {
if (!error && data) $cache.delete('defer_' + token, function () {})
next(error, data)
}) */
$cache.get('defer_' + token, next)
}
/**
* Renomme un groupe chez toutes les ressources qui l'ont (dans groupes ou groupesAuteurs)
* @param oldName
* @param newName
* @param next
*/
function renameGroup (oldName, newName, next) {
const limit = 100
const modifier = (nom) => nom === oldName ? newName : nom
const updateGroupes = () => {
flow().seq(function () {
EntityRessource.match('groupes').equals(oldName).grab({ limit, offset }, this)
}).seqEach(function (ressource) {
ressource.groupes = ressource.groupes.map(modifier)
ressource.groupesAuteurs = ressource.groupesAuteurs.map(modifier)
save(ressource, this)
}).seq(function (ressources) {
if (ressources.length < limit) {
// on a fini
offset = 0
return updateGroupesAuteurs()
}
// faut refaire un tour
offset += limit
updateGroupes()
}).done(next)
}
const updateGroupesAuteurs = () => {
flow().seq(function () {
// pour les groupes auteur qui ne publiaient pas dans leur groupe
EntityRessource.match('groupesAuteurs').equals(oldName).grab({ limit, offset }, this)
}).seqEach(function (ressource) {
ressource.groupesAuteurs = ressource.groupesAuteurs.map(modifier)
save(ressource, this)
}).seq(function (ressources) {
if (ressources.length < limit) return next()
offset += limit
updateGroupesAuteurs()
}).done(next)
}
let offset = 0
updateGroupes()
}
/**
* Ajoute ou modifie une ressource (contrôle la validité avant et incrémente la version au besoin),
* met à jour le cache (interne + varnish) et toutes les relations (passe en revue tous les éventuels
* arbres qui référencent cette ressource)
*
* ATTENTION, c'est la seule méthode qui garanti l'intégrité du cache, entityRessource.store()
* utilisé directement peut être plus efficace pour du batch, mais faut y réfléchir à deux fois !
* @memberOf $ressourceRepository
* @param {EntityRessource} ressource
* @param {ressourceCallback} [next] appelée avec une EntityRessource
*/
function save (ressource, next) {
flow().seq(function () {
if (!isEntity(ressource, 'EntityRessource')) {
log.debug('save d’une ressource qui n’est pas une entity', ressource)
// ça sort pas de la base, donc on le crée…
ressource = EntityRessource.create(ressource)
}
// difficile de savoir si ça sort de la base ou si c'est un objet avec oid passé au create
// car dans les deux cas onLoad est appelé et ça génère un $original, qui n'est donc pas forcément
// ce qu'il y a en base (faudra fixer ça dans lassi, mais y'avait une raison pour l'appeler sur
// du create avec oid…)
// donc si y'a un oid on refait un load car on veut dans $original ce qui sort de la base,
// c'est important pour checkAgainstPrevious
// (et si on venait d'aller le chercher en base il est en cache donc c'est pas trop cher)
if (ressource.oid) load(ressource.oid, this)
else this()
}).seq(function (ressourceBdd) {
if (ressourceBdd) checkAgainstPrevious(ressource, this)
else this(null, ressource)
}).seq(function (ressource) {
if (ressource.type === 'ec2' && ressource.parametres && ressource.parametres.xml) {
convertXmlEc2(ressource)
} else if (ressource.type === 'j3p' && ressource.parametres && ressource.parametres.xml) {
convertXmlJ3p(ressource)
log.debug('ressource j3p après conversion avant write', ressource)
}
beforeSave(ressource, this)
}).seq(function (ressource) {
ressource.store(this)
}).seq(function (ressource) {
if (!ressource.oid) throw new Error('Après un write la ressource n’a pas d’oid')
// mise en cache, purge varnish et passage au suivant
// gestion du cache pas possible en afterStore car le cache dépend de l'entité.
// C'est aussi plus logique que $ressourceRepository gère cache + intégrité croisée des données,
// et le gestionnaire d'entité seulement l'intégrité interne des données d'une entité
$cacheRessource.set(ressource)
purgeVarnish(ressource)
log.debug('write ' + ressource.oid + ' ok')
if (next) next(null, ressource)
}).catch(function (error) {
// on log toujours ici
log.error(error)
if (next) next(error)
})
} // save
/**
* Récupère un liste de ressource d'après critères
* @memberOf $ressourceRepository
* @param {searchQuery} searchQuery Les critères de tri
* @param {searchQueryOptions} queryOptions Les options (skip & limit + orderBy éventuel)
* @param {ressourcesCallback} next appelée avec (error, {ressources, total})
*/
function search (searchQuery, queryOptions, next) {
grabSearchCount(searchQuery, function (error, total) {
if (error) return next(error)
if (total === 0) return next(null, { ressources: [], total })
grabSearch(searchQuery, queryOptions, function (error, ressources) {
if (error) return next(error)
next(null, { ressources, total })
})
})
}
/**
* Met à jour les arbres ou séries stockés ici qui ont ref comme enfant
* @param {Ref} ref
* @param {function} next appelée avec (error, nbArbres)
*/
function updateParent (ref, next) {
// cherche un enfant et le modifie si besoin, retourne true si on a fait une modif
function findChild (arbre) {
if (arbre.enfants && arbre.enfants.length) {
// avec Array.some on pourrait sortir dès qu'on a trouvé, mais un arbre pourrait avoir
// deux fois le même enfant, on les parse tous
arbre.enfants.forEach((enfant, index) => {
if (enfant.aliasOf === rid) arbre.enfants[index] = ref
else if (enfant.enfants && enfant.enfants.length) findChild(enfant)
})
}
}
// on cherche les enfants d'après le rid de la ressource
const rid = ref.aliasOf
let nbArbres = 0
flow().seq(function () {
// on cherche nos arbres contenant cet enfant
EntityRessource.match('enfants').equals(rid).grab(this)
}).seqEach(function (arbre) {
nbArbres++
findChild(arbre)
save(arbre, this)
}).seq(function () {
if (next) next(null, nbArbres)
}).catch(next || log.error)
}
/**
* Service d'accès aux ressources, utilisé par les différents contrôleurs
* @service $ressourceRepository
* @requires EntityRessource
* @requires EntityArchive
* @requires $ressourceControl
* @requires $cacheRessource
* @requires $cache
*/
return {
archive,
getDeferred,
grabSearch,
grabSearchCount,
load,
loadByAliasAndPid,
loadByCle,
loadByOrigin,
remove,
removeGroup,
renameGroup,
save,
saveDeferred,
search,
updateParent
}
})
}
/**
* @callback ressourcesCallback
* @param {Error} error
* @param {Ressource[]} ressources
*/
/**
* Tableau de filtres
* @typedef {getListeFilter[]} getListeFilters
*/
/**
* Filtre de la forme {index:'indexAFiltrer', values:valeurs},
* valeurs peut être un tableau de valeurs ou [undefined] (ça filtrera sur les ressources ayant cet index)
* @typedef {{index: string, values: Array}} getListeFilter
*/