Mongoose: nouvelle fonctionnalité : async virtuel

Créé le 27 oct. 2017  ·  42Commentaires  ·  Source: Automattic/mongoose

Nouvelle fonctionnalité virtual async , s'il vous plaît support!

const User = new Schema(
  {
    username: {
      type: String,
      index: true,
      unique: true
    },
    encryptedPassword: {
      type: String,
      required: true,
      minlength: 64,
      maxlength: 64
    },
    passwordSalt: {
      type: String,
      required: true,
      minlength: 32,
      maxlength: 32
    }
})

User.virtual('password').set(async function generate(v) {
  this.passwordSalt = await encryptor.salt()
  this.encryptedPassword = await encryptor.hash(v, this.passwordSalt)
})
  const admin = new User({
    username: 'admin',
    password: 'admin'
  })
  admin.save()

Commentaire le plus utile

De très bonnes idées dans votre code, nous avons vu ce problème à quelques reprises mais n'avons pas vraiment eu le temps de l'étudier. J'aime cette idée cependant, je la considérerai pour une prochaine version

Tous les 42 commentaires

Courant, j'utilise une sale manière :

User.virtual('password').set(function(v) {
  this.encryptedPassword = v
})

User.pre('validate', function preValidate(next) {
  return this.encryptPassword().then(next)
})

User.method('encryptPassword', async function encryptPassword() {
  this.passwordSalt = await encryptor.salt()
  this.encryptedPassword = await encryptor.hash(
    this.encryptedPassword,
    this.passwordSalt
  )
})

+1

+1

De très bonnes idées dans votre code, nous avons vu ce problème à quelques reprises mais n'avons pas vraiment eu le temps de l'étudier. J'aime cette idée cependant, je la considérerai pour une prochaine version

le problème est... à quoi ressemble la syntaxe d'utilisation ?

await (user.password = 'some-secure-password');

Cela ne fonctionne pas.

Selon ECMA262 12.15.4 , la valeur de retour de user.password = 'some-secure-password' devrait être _rval_, qui est dans ce cas 'some-secure-password' .

Vous proposez que la valeur de retour de someVar = object soit un Promise , et selon ce fil et la spécification ES262 liée ci-dessus, il s'agit d'une "violation profonde de la sémantique ES".

De plus, tenter d'implémenter un tel problème de violation sémantique dans le simple but d'avoir une fonction de commodité est une assez mauvaise idée, d'autant plus que cela pourrait potentiellement signifier toutes sortes de mauvaises choses pour la base de code de la mangouste dans son ensemble.

Pourquoi ne fais-tu pas simplement :

const hashPassword = require('./lib/hashPassword');

const password = await hashPassword('some-secure-password');
User.password = password; // This is completely normal.

Il n'y a littéralement pas besoin d'essayer de faire un setter async , ce qui ne devrait pas être fait en premier lieu, pour un one-liner aussi simple que celui-ci.

Vous pouvez aussi simplement faire ceci :

User.methods.setPassword = async function (password) {
  const hashedPassword = await hashPassword(password);
  this.password = hashedPassword;
  await this.save();
  return this;
};
const myUser = new User();
await myUser.setPassword('mypassword...');

Je n'ai aucune idée de pourquoi vous vous donneriez la peine de faire des virtuels, des crochets de pré-sauvegarde, etc.

Je suis d'accord avec @heisian. Cela ressemble à un ballonnement de fonctionnalité/api pour moi à mon humble avis. Je ne vois pas en quoi l'alternative d'utiliser une méthode d'instance est gênante ici. Mais ajouter un support de syntaxe assez important pour cela ressemble vraiment à du ballonnement.

Nous devrions avoir une fonctionnalité très simple comme celle-ci :

User.virtual('password').set((value, done) => {
  encryptValueWithAsyncFunction
    .then(response => done(null, value))
    .catch(reason => done(reason))
  ;
})

@gcanu vous

Qu'y a-t-il de mal à simplement faire :

await User.setPassword('password');

???

Au cas où vous ne l'auriez pas vu avant, cela ne fonctionnera pas :

await (User.password = 'password');

@vkarpov15 Il ne s'agit pas d'un problème spécifique à la mangouste, mais plutôt d'une remise en question de la validité de la spécification ECMAScript actuelle. Cette "demande de fonctionnalité" devrait être fermée...

Le code ci-dessous est une très mauvaise idée ! Pourquoi la définition du mot de passe inclut l'opération save ?

User.methods.setPassword = async function (password) {
  const hashedPassword = await hashPassword(password);
  this.password = hashedPassword;
  await this.save();
  return this;
};

const myUser = new User();
await myUser.setPassword('mypassword...');

La mangouste a besoin de plus moderne, plus élégante.

@heisian Ok mon erreur, je ne me suis pas occupé de l'utilisation du passeur...

@heisian Plz voir https://github.com/Automattic/mongoose/blob/master/lib/virtualtype.js.

Actuellement, dans Mongoose IMPL, getter ou setter enregistrent simplement une fonction puis appellent, ce n'est pas https://tc39.github.io/ecma262/#sec -assignment-operators-runtime-semantics -evaluation et https://github.com/tc39/ecmascript-asyncawait/issues/82. C'est différent.

Alors s'il vous plaît ouvrez cette demande.

@fundon , Dites-moi ceci : Comment allez-vous exactement appeler votre passeur virtuel ? Veuillez montrer l'utilisation. Si vous utilisez async cela doit être géré par une promesse. Votre exemple d'origine n'affiche await nulle part dans l'appel de passeur/d'affectation.

Mon exemple de code n'est qu'un exemple... vous pouvez aussi le faire si facilement :

User.methods.setPassword = async function (password) {
  const hashedPassword = await hashPassword(password);
  this.password = hashedPassword;
  return this;
};

const myUser = new User();
await myUser.setPassword('mypassword...');
await myUser.save();

Évidemment..

Votre exemple n'est pas un bon moyen pour moi.

je veux

await new User({ password }).save()

Hachez le mot de passe dans le mode le plus simple, le plus élégant.

Pourquoi? afin que vous puissiez enregistrer quelques lignes de code? la raison n'est pas suffisante pour justifier tout le travail supplémentaire et éventuellement les modifications de rupture apportées à la base de code.

Vous devez également réaliser à la fin de la journée, peu importe comment vous le formulez, ce qui se passe en interne dans Mongoose est un setter, qui ne peut pas être asynchrone/attendre.

Je ne suis pas d'accord avec @heisian. Mongoose a trop de vieilles choses. La mangouste a besoin d'être refactorisée !
La mangouste a besoin de modernité.

Si ce problème est clos. Je vais fork Mongoose, refactorisez-le ! Au revoir!

Super! C'est le but de l'open source. S'il vous plaît, allez-y et créez une fourchette avec une version réduite, ce serait bon pour nous tous.

Il n'y a vraiment aucun souci à avoir besoin de await (User.password = 'password'); . Le seul inconvénient réel est que user.password = 'password'; signifiera alors qu'il y a une opération asynchrone en cours, donc user.passwordSalt ne sera pas défini. Le rapport avec les hooks est également une question intéressante : que se passe-t-il si vous avez un hook pre('validate') ou pre('save') , ceux-ci doivent-ils attendre que l'opération async user.password soit terminée ?

Je ne suis pas enclin à écarter cette question du revers de la main. Il y a beaucoup de valeur à avoir dans la consolidation du comportement asynchrone derrière .save() , .find() , etc. il suffit de s'assurer qu'il s'intègre parfaitement avec le reste de l'API.

Aujourd'hui, les getters et setters asynchrones sont très importants pour moi. J'ai besoin d'envoyer des requêtes HTTP des getters et setters pour déchiffrer/chiffrer des champs avec des méthodes de chiffrement propriétaires, et actuellement, il n'y a aucun moyen de le faire. Avez-vous une idée de comment y parvenir ?

@gcanu, je les implémenterais simplement en tant que méthodes

Pour les raisons mentionnées et le fait qu'il existe des méthodes pour gérer facilement toutes les opérations asynchrones dont vous avez besoin, je ne vois aucun utilitaire derrière la consolidation du comportement async .. encore une fois, await (User.password = 'password') enfreint la convention ECMAScript et je garantir que cela va être difficile et ne vaut pas la peine d'être mis en œuvre avec élégance...

Vous avez raison, ce modèle n'est pas quelque chose que nous allons mettre en œuvre. L'idée d'attendre que le virtuel asynchrone se résolve avant de sauvegarder est intéressante.

Je l'adorerais pour une implémentation de toJSON({virtuals: true}) . Certains des champs virtuels que j'obtiens en exécutant d'autres requêtes sur la base de données, que je ne souhaite exécuter qu'une fois que vous avez sérialisé.

@gabzim ce serait assez compliqué car JSON.stringify ne prend pas en charge les promesses. Ainsi, res.json() ne pourra jamais gérer les virtuels asynchrones à moins que vous n'ajoutiez des aides supplémentaires à exprimer.

Ah ouais, c'est logique, merci @vkarpov15

Serait-ce une bonne pratique de faire une requête dans le rappel get ?
Je pense que ce serait utile dans certains cas.

Disons que je veux obtenir le chemin complet d'une page Web (ou d'un document), où les documents peuvent être imbriqués, quelque chose comme les chemins d'URL Github.

const Doc = require('./Doc.js');
//...
subDocSchema.virtual('fullpath').get(async function(){
    const doc = await Doc.findById(this.doc); //doc is a Doc ref of type _id
    return `/${ doc.path }/${ this.path }`
})

Ici, nous devons utiliser async/await car les opérations de requête sont asynchrones.

@JulianSoto dans ce cas, je vous recommande d'utiliser une méthode plutôt qu'un virtuel. La principale raison d'utiliser des virtuels est que vous pouvez faire en sorte que Mongoose inclue des virtuels dans les sorties toJSON() et toObject() . Mais toJSON() et toObject() sont synchrones, ils ne géreraient donc pas le virtuel asynchrone pour vous.

J'ai également rencontré un cas d'utilisation pour cela, et j'essaie de réfléchir à une bonne solution, très ouverte aux idées, et d'accord pour ne pas casser la sémantique du setter.

J'ai écrit un utilitaire pour appliquer automatiquement les ensembles de correctifs JSON aux modèles de mangouste. Il prend en charge le remplissage automatique avec des chemins profonds : https://github.com/claytongulick/mongoose-json-patch

L'idée est qu'en combinaison avec certaines règles : https://github.com/claytongulick/json-patch-rules, vous pouvez être assez proche d'avoir une API « automatique » avec JSON Patch.

Mon plan était que pour les cas où une simple affectation ne fonctionnerait pas, d'utiliser des virtuels. Lorsque le correctif est appliqué, un virtuel récupérera tout ce que vous voulez - cela permettrait à votre objet d'interface d'être différent du modèle / objet db réel de la mangouste.

Par exemple, j'ai un objet User qui prend en charge une opération « add » sur « payment_methods ». L'ajout d'un mode de paiement n'est pas un ajout direct à un tableau - c'est un appel au processeur avec un jeton de paiement, récupérer un jeton de mode de paiement, le stocker d'une manière différente dans le modèle, etc...

Mais j'aimerais que le modèle d'interface, le modèle conceptuel, puisse être patché avec un patch JSON 'add' op.

Sans setters asynchrones, cela ne fonctionnera pas. Je suppose que la seule option est que mongoose-json-patch accepte en option une sorte de mappage entre les chemins, les opérations et les méthodes de mongoose, à moins qu'il n'y ait de meilleures idées?

@claytongulick pourquoi avez-vous besoin d'un setter asynchrone plutôt que de await sur une opération asynchrone, puis défini de manière synchrone ?

@vkarpov15 Et rendiez simplement toObject() et toJSON() asynchrone par défaut et introduisiez les fonctions toObjectSync() et toJSONSync() ? Sync variantes async virtuels. (Je me souviens que ce motif est utilisé quelque part dans la mangouste, donc ce ne serait pas trop bizarre à avoir.)

Mon cas d'utilisation est quelque chose comme ceci : j'ai un schéma qui a un virtuel qui fait un find() sur un autre modèle (un peu plus complexe que de simplement remplir un identifiant). Bien sûr, je peux dénormaliser les éléments que je veux dans mon modèle principal à l'aide de crochets de sauvegarde/suppression, mais cela entraîne de nombreux coûts de maintenabilité (et je n'ai vraiment pas besoin des avantages en termes de performances dans ce cas particulier). Il me semble donc naturel d'avoir un virtuel pour le faire pour moi.

JSON.stringify() ne prend pas en charge l'async toJSON() , donc malheureusement l'idée toJSONSync() ne fonctionnera pas.

Je sais que vous avez dit que votre find() est assez complexe, mais vous voudrez peut-être jeter un œil à remplir les virtuals juste au cas où. Vous pouvez également essayer un middleware de requête.

De plus, votre virtuel async a-t-il un setter ou seulement un getter @isamert ?

Une solution pour ceux qui ont ce problème :

Dans le cas où seul le setter est asynchrone, j'ai trouvé une solution. C'est un peu sale mais ça a l'air de bien fonctionner.
L'idée est de passer au setter virtuel un objet contenant un résolveur de promesse comme accessoire de rappel et la propriété virtuelle à définir. lorsque le setter a terminé, il appelle le callback, ce qui signifie à l'extérieur que l'objet peut être sauvegardé.

Pour utiliser un exemple de base inspiré de la première question :

const User = new Schema(
  {
    username: {
      type: String,
      index: true,
      unique: true
    },
    encryptedPassword: {
      type: String,
      required: true,
      minlength: 64,
      maxlength: 64
    }
})

User.virtual('password').set(function generate(inputWithCb, virtual, doc) {
  let cb = inputWithCb.cb;
  let password = inputWithCb.password;
  encryptor.hash(password)
  .then((hash) => {
    doc.set("encryptedPassword", hash);
    cb && cb();
  });
})
// create the document
const admin = new User({
  username: 'admin'
});
// setup the promise for setting the async virtuals
const pwdProm = new Promise((resolve) => {
  admin.set("password", {cb: resolve, password: "admin"});
})

//run the promise and save only when the virtual setters have finished executing
pwdProm
.then(() => {
  admin.save();
});

Cela pourrait avoir des conséquences indésirables, alors utilisez-le à vos risques et périls.

@silto pourquoi n'utilisez-vous pas simplement une méthode de schéma qui renvoie une promesse ?

@vkarpov15 Je le ferais normalement, mais dans le projet où je l'ai fait, j'ai des schémas, des virtuels et des points de terminaison graphQL générés automatiquement à partir d'un "plan" json, donc je préfère avoir une interface virtuelle uniforme au lieu d'une méthode pour un cas particulier.

@silto pouvez-vous fournir des exemples de code ? j'aimerais bien voir à quoi cela ressemble

Dans le cas de setter, vous voudrez peut-être l'enregistrer ou simplement rechercher les données du document, si vous enregistrez, c'est une promesse, vous pouvez donc vérifier les champs qui sont des promesses, les résoudre, puis enregistrer.

Si vous souhaitez rechercher les données, vous pouvez définir par des options de schéma que ce modèle sera de type Promesse ou lorsque vous créez le modèle, vérifiez le schéma et vérifiez s'il existe un setter, un getter ou un virtuel qui est une promesse, puis tournez en une promesse.

Ou vous pouvez simplement utiliser une fonction de type exec que vous possédez déjà (execPopulate).

en résumé, si vous voulez observer les données qui ont un setter, getter ou virtual, vous pouvez créer une fonction pour ce qui compte, si vous voulez enregistrer les données c'est déjà une promesse donc vous pouvez utiliser la même fonction pour transformer les données avant de l'enregistrer.

J'avais l'habitude d'utiliser des virtuels avec des promesses, mais comme j'utilise des promesses express, presque tout le temps, je ne me soucie pas des promesses, mais dans certains cas, j'utilise Doc..then(), comme je n'ai jamais utilisé de setters avec des promesses, je n'ai pas ce problème...

Quoi qu'il en soit, ce serait bien d'avoir une sorte de wrapper pour obtenir toutes les données déjà résolues et de ne pas avoir à utiliser le "then" sur chaque virtuel et getter promis, ou après avoir défini un setter promis.

Si vous le souhaitez, je peux vous aider dans cette démarche.

Meilleures salutations.

PS : un exemple typique d'utilisation de virtuels promisifés, dans mon cas j'utilise 2 chemins pour savoir si mes Documents peuvent être supprimés ou mis à jour selon des données externes, j'ai donc généralement besoin d'interroger d'autres modèles pour savoir si celui-ci peut être supprimé ou modifié . Comme je l'ai déjà dit, la promesse expresse résout ce problème pour moi, mais si je veux vérifier en interne si ces tâches peuvent être effectuées, je devais résoudre cette promesse auparavant.

@chuager pouvez-vous s'il vous plaît fournir des exemples de code ?

Salut, par exemple, selon mon commentaire ci-dessous, j'utilise 2 virtuals _update et _delete, et un plugin qui définit ces virtuals au cas où il ne serait pas défini dans le schéma, retournant true.

J'ai un modèle Simulation pour définir un crédit, et un mode Projet pour publier la simulation avec les données mkt.
La simulation ne peut pas être supprimée s'il y a un projet associé à la simulation, et ne peut pas être mise à jour si le projet est publié pour des investissements.

La résolution de la mise à jour virtuelle dans la simulation se fait en trouvant un projet avec la simulation référencée et le statut est "En Financiamiento", si cette requête est vraie alors la simulation ne peut pas être mise à jour... évidemment la "trouver" est un promis, donc le virtuel c'est aussi...

Comme j'utilise normalement ce virtuel dans le frontend, les données sont analysées par un module qui résout l'objet (co ou express-promesse selon est un ou un tableau de résultat).

Dans le cas où je voulais voir le document, je découvrirai que mes virtuels sont des promesses, je dois donc utiliser le module co pour résoudre, mais je devais déjà utiliser le résultat comme promesse... peut-être en ajoutant simplement co au résultat fera la magie, ou avec un plugin qui utilise co après find... mais il semble plus naturellement que le jeu de résultats a déjà fait le travail.

J'utilise beaucoup de points de terminaison pour obtenir des données de la mangouste, si je devrai utiliser cette fonction partout ou utiliser un crochet de poste pour rechercher.

même chose avec les getters, avec les setters, le crochet doit être pré-validation, mais il est important de ne pas toucher aux autres accessoires du document, car il a des références circulaires et d'autres accessoires comme le constructeur.

Salutations...

PS : Si vous avez vraiment besoin d'un exemple de code, faites-le moi savoir.

@chuager big wall of prose !== exemple de code. Je préférerais vraiment un exemple de code.

Cette page vous a été utile?
0 / 5 - 0 notes