Imports de données
user-friendly
avec Loopback
contexte
Projet AGILE de 9 moiS
construction d'un outils metier pour remplacer
fichier à importer régulièrement
audits de sécurité
HYPOTHÈSES
UN SEUL LANGAGE POUr LE BACKEND
développement réalisé en agile, sprints d'une semaine
Le client veut passer peu de temps sur l'import de données
L'IMPORT DE DONNÉES SERA RÉALISÉ PAR UN UTILISATEUR DU MÉTIEr
pierre
Pierre est chef de projet informatique
Pierre est trentenaire, éduqué, "digital native"
Pierre a des notions sur les bases de données
Pierre maitrise excel
L'import a échoué, le fichier est incorrect
pierre ne comprend pas:
alors pierre ré-essaye
possibilité de se tromper sans corrompre la base
LE DROIT DE SE TROMPER
autonomie pour corriger LES ERREURS DANS LES données
UTILISATEUR INFORMÉ DE L'ÉTAT SON UPLOAD DE FICHIERS
Pour pierre
pas de dépendance envers une équipe de développeurs
correspond aux standards du marché en ux
autonomie:
qualité de service:
Pour les devs
pas de responsabilité après la fin du projet
pas besoin de tester en local
pas de perte de temps pour des équipes de support de charger des fichiers
pas processus difficile à débugger
DEMO
1. upload d'un fichier CSV
2. fork d'un process node
3. STOCKEr EN BASE L'ÉTAT DE L'IMPORT: 'EN COURS'
4. debut d'une transaction SQL
5. execution du process d'import
stocker les erreurs
rollback
status = 'error'
commit transaction
status = 'sucess'
les transactions sql
Connecteurs pour MySQL, PostgreSQL, SQL Server,Oracle connector
Read Committed: un SELECT voit seulement les données non commitées
Cheese.beginTransaction(
{
isolationLevel: Post.Transaction.READ_COMMITTED
}, function(err, tx) {
// Now we have a transaction (tx)
});
Cheese.import = function(file, callback) {
var tx;
tx = {};
return Model.startTransaction(tx, file)
.then(function() {
return Cheese.importProcess(tx, file);
})
.then(function() {
return tx.commit(function(err) {
return Cheese.exitAfterSuccess(callback);
});
})
.catch(function(err) {
return tx.rollback(function(rollbackError) {
return Cheese.exitAfterError(err, callback);
});
});
};
process d'import
dans cheese.js
resultat
deuxième étape
un fichier
-
2 fichiers = 2 méthodes
-
3 fichiers = 3 méthodes
- ...
les mixins loopback
ajouter une méthode à plusieurs modèles
{
"_meta": {
...
"mixins": [
...
"./mixins"
]
}
}
model-config.json
common/mixins/upload.js
Model.import = function(file, callback) {
var tx;
tx = {};
return Model.startTransaction(tx, file)
.then(function() {
return Model.importProcess(tx, file);
})
.then(function() {
return tx.commit(function(err) {
return Model.exitAfterSuccess(callback);
});
})
.catch(function(err) {
return tx.rollback(function(rollbackError) {
return Model.exitAfterError(err, callback);
});
});
};
les mixins loopback
ajouter une méthode à plusieurs modèles
common/mixins/upload.js
Model.import = function(file, callback) {
var tx;
tx = {};
return Model.startTransaction(tx, file)
.then(function() {
return Model.importProcess(tx, file);
})
.then(function() {
return tx.commit(function(err) {
return Model.exitAfterSuccess(callback);
});
})
.catch(function(err) {
return tx.rollback(function(rollbackError) {
return Model.exitAfterError(err, callback);
});
});
};
{
"name": "Cheese",
"base": "PersistedModel",
"mixins": {
"Upload": {},
},
cheese.json
overrider la logique métier dans le model
if (!Model.importLine) {
Model.importLine = function(line) {
return new Promise(function(resolve, reject) {
return resolve();
});
};
}
common/mixins/upload.js
Cheese.importLine = function(transaction, line) {
return Cheese.findOne({
where: {
name: line.CheeseName
}
}, transaction
).then(function(foundCheese) {
var cheese;
cheese = {
name: line.CheeseName
};
if (foundCheese) {
cheese.id = cheese.id;
}
return Cheese.upsert(cheese, transaction);
});
};
common/cheese.js
paramétrisation
Description des colonnes de l'excel dans le model.json
{
"name": "Cheese",
"base": "PersistedModel",
"mixins": {
"Upload": {},
"CsvUpload": {
"requiredHeaders": [
"cheeseName",
"animalMilk"
],
"validators": {
"cheeseName": {
"required": true,
"type": "string"
},
"animalMilk": {
"required": true,
"type": "string"
}
}
},
module.exports = function(Model, options) {
if (!Model.validateLine) {
return Model.validateLine = function(line) {
return importService.validate(line, options.validators);
}
};
}
};
un fichier csv a importer = un modèle loopback
du CSV au XLS
{
"name": "Cheese",
"base": "PersistedModel",
"mixins": {
"Upload": {},
"XlsUpload": {
"requiredHeaders": [
"cheeseName",
"animalMilk"
],
"validators": {
"cheeseName": {
"required": true,
"type": "string"
},
"animalMilk": {
"required": true,
"type": "string"
}
}
},
un modèle != une table en base
difficultés techniques
PROCESS node forké pas tué
erreurs sql mal gérées
certaines erreurs manquées
callbacks
+
gestion d'erreurs chainée
complexité
tout promissifier!
loopback-promifisfy
Loopback 3.0
Model.import = function(file, callback) {
var tx;
tx = {};
return Model.startTransaction(tx, file)
.then(function() {
return Model.importProcess(tx, file);
})
.then(function() {
return Model.exitAfterSuccess(callback);
});
})
.catch(function(err) {
return Model.exitAfterError(err, callback);
});
});
};
bilan
les utilisateurs testent l'upload trop tard
permettre de choisir le delimiteur
permettre l'import .xls .xlsx
règles de validations souples
réparation automatique d'erreurs ?
Pour aller plus loin...
sécurité: détection du type de fichiers
ux: détection de l'encodage
Merci
DES QUESTIONS ?
[FastIT] Imports de données User-friendly avec Loopback
By Clément Ricateau
[FastIT] Imports de données User-friendly avec Loopback
- 1,138