Mongoose: neues Feature: Virtual Async

Erstellt am 27. Okt. 2017  ·  42Kommentare  ·  Quelle: Automattic/mongoose

Neue Funktion virtual async , PLZ-Unterstützung!

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()

Hilfreichster Kommentar

Einige sehr gute Ideen in Ihrem Code. Wir haben dieses Problem einige Male gesehen, hatten aber noch keine Zeit, es zu untersuchen. Ich mag diese Idee jedoch, werde es für eine kommende Veröffentlichung in Betracht ziehen

Alle 42 Kommentare

Aktuell benutze ich einen schmutzigen Weg:

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

Einige sehr gute Ideen in Ihrem Code. Wir haben dieses Problem einige Male gesehen, hatten aber noch keine Zeit, es zu untersuchen. Ich mag diese Idee jedoch, werde es für eine kommende Veröffentlichung in Betracht ziehen

Das Problem ist.. Wie sieht die Syntax der Verwendung aus?

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

Dies funktioniert nicht.

Gemäß ECMA262 12.15.4 sollte der Rückgabewert von user.password = 'some-secure-password' _rval_ sein, was in diesem Fall 'some-secure-password' .

Sie schlagen vor, dass der Rückgabewert von someVar = object ein Promise , und gemäß diesem Thread und der oben verlinkten ES262-Spezifikation ist dies eine "tiefe Verletzung der ES-Semantik".

Darüber hinaus ist der Versuch, ein solches semantisch verletzendes Problem nur zu dem Zweck zu implementieren, eine Komfortfunktion zu haben, eine ziemlich schlechte Idee, zumal dies möglicherweise alle möglichen schlechten Dinge für die Mungo-Codebasis als Ganzes bedeuten könnte.

Warum machst du nicht einfach:

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

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

Es besteht buchstäblich keine Notwendigkeit, für einen so einfachen Einzeiler wie diesen einen async Setter zu erstellen, was von vornherein nicht getan werden sollte.

Du könntest auch einfach so machen:

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...');

Ich habe keine Ahnung, warum Sie sich die Mühe machen sollten, Virtuals zu erstellen, Hooks vor dem Speichern usw.

Ich stimme @heisian zu. Das fühlt sich für mich IMHO wie ein Feature/Api-Aufblähen an. Ich sehe nicht, wie die Alternative zur Verwendung einer Instanzmethode hier unpraktisch ist. Aber das Hinzufügen einer ziemlich großen Syntaxunterstützung dafür fühlt sich definitiv wie Aufgeblähtheit an.

Wir sollten eine sehr einfache Funktion wie diese haben:

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

@gcanu Sie ignorieren völlig, was ich gepostet habe, was Sie vorschlagen, gibt ein Versprechen von einem Zuweisungsaufruf zurück und das verstößt vollständig gegen die Javascript / ECMA262-Spezifikation. Damit Ihr Code-Snippet funktioniert, muss Ihre Setter-Funktion ein Promise sein, was per Definition per Spezifikation nicht erlaubt ist und sowieso nicht funktionieren würde.

Was ist falsch daran, einfach zu tun:

await User.setPassword('password');

???

Falls Sie es noch nicht gesehen haben, funktioniert dies

await (User.password = 'password');

@vkarpov15 Dies ist kein Mungo-spezifisches Problem, sondern stellt eher die Gültigkeit der aktuellen ECMAScript-Spezifikation in Frage. Diese "Funktionsanfrage" sollte geschlossen werden...

Der folgende Code ist eine sehr schlechte Idee! Warum beinhaltet das Festlegen des Passworts die Operation 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...');

Mungo braucht moderner, eleganter.

@heisian Ok, mein Fehler, ich habe mich nicht um den Setter-Einsatz gekümmert ...

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

Derzeit registriert in Mongoose IMPL getter oder setter einfach eine Funktion und ruft sie dann auf, es ist nicht https://tc39.github.io/ecma262/#sec -assignment-operators-runtime-semantics -Auswertung und https://github.com/tc39/ecmascript-asyncawait/issues/82. Das ist anders.

Also bitte öffnen Sie diese Anfrage.

@fundon , Sag mir das: Wie genau wirst du deinen virtuellen Setter nennen? Bitte zeigen Sie die Verwendung an. Wenn Sie async , muss dies durch ein Versprechen gehandhabt werden. In Ihrem ursprünglichen Beispiel wird await nirgendwo im Setter-/Zuweisungsaufruf angezeigt.

Mein Beispielcode ist nur ein Beispiel ... Sie können dies auch so einfach tun:

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();

Offensichtlich..

Dein Beispiel ist für mich kein guter Weg.

Ich möchte

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

Hash das Passwort in dem Modus, der einfacher und eleganter ist.

warum? damit Sie ein paar Zeilen Code sparen können? der Grund reicht nicht aus, um die ganze zusätzliche Arbeit und möglicherweise fehlerhafte Änderungen an der Codebasis zu rechtfertigen.

Sie müssen auch am Ende des Tages erkennen, egal wie Sie es formulieren, was intern bei Mongoose vor sich geht, ist ein Setter, der nicht asynchron/abwartend sein kann.

Ich stimme @heisian nicht zu. Mungo hat zu viele alte Dinge. Mungo braucht Refactoring!
Mungo braucht moderne.

Wenn dieses Problem geschlossen ist. Ich werde Mungo abzweigen, umgestalten! Wiedersehen!

Groß! Das ist der Punkt von Open Source. Bitte machen Sie weiter und erstellen Sie eine Gabel mit einer abgespeckten Version, es wäre gut für uns alle.

Es gibt wirklich keine Bedenken, await (User.password = 'password'); benötigen. Der einzige wirkliche Nachteil ist, dass user.password = 'password'; dann bedeutet, dass ein asynchroner Vorgang stattfindet, sodass user.passwordSalt nicht festgelegt wird. Wie das mit Hooks zusammenhängt, ist auch eine interessante Frage: Was passiert, wenn Sie einen pre('validate') oder pre('save') Hook haben, sollten diese warten, bis die asynchrone Operation user.password ist?

Ich bin nicht geneigt, dieses Thema von der Hand zu weisen. Die Konsolidierung des asynchronen Verhaltens hinter .save() , .find() usw. ist sehr wertvoll. Sie müssen nur sicherstellen, dass es gut zum Rest der API passt.

Heute sind mir asynchrone Getter und Setter sehr wichtig. Ich muss HTTP-Anfragen von Gettern und Settern senden, um Felder mit proprietären Verschlüsselungsmethoden zu entschlüsseln / zu verschlüsseln, und derzeit gibt es keine Möglichkeit, dies zu tun. Haben Sie eine Idee, wie Sie das erreichen können?

@gcanu Ich würde diese einfach als Methoden implementieren

Für meine Gründe genannt und die Tatsache , dass es Methoden gibt , um bequem alle Asynchron - Operationen behandeln Sie brauchen, sehe ich keinen Nutzen hinter Konsolidierung async Verhalten .. wieder, await (User.password = 'password') Pausen ECMAScript Konvention und ich garantieren, dass es schwierig sein wird und es nicht wert ist, elegant zu implementieren ...

Sie haben Recht, dieses Muster werden wir nicht implementieren. Die Idee, vor dem Speichern darauf zu warten, dass die asynchrone virtuelle Lösung aufgelöst wird, ist interessant.

Ich würde es lieben für eine toJSON({virtuals: true}) Implementierung. Einige der virtuellen Felder erhalte ich, indem ich andere Abfragen an die Datenbank ausführe, die ich erst nach der Serialisierung ausführen möchte.

@gabzim das wäre ziemlich chaotisch, weil JSON.stringify keine Versprechen unterstützt. res.json() kann also niemals asynchrone virtuelle Dateien verarbeiten, es sei denn, Sie fügen zusätzliche Helfer zum Ausdruck hinzu.

Ah ja, macht Sinn, danke @vkarpov15

Wäre es eine gute Vorgehensweise, eine Abfrage innerhalb des get Rückrufs durchzuführen?
Ich denke, das wäre in einigen Fällen nützlich.

Nehmen wir an, ich möchte den vollständigen Pfad einer Webseite (oder eines Dokuments) abrufen, in der Dokumente verschachtelt werden können, etwa Github-URL-Pfade.

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 }`
})

Hier müssen wir async/await verwenden, da Abfragevorgänge asynchron sind.

@JulianSoto In diesem Fall empfehle ich Ihnen, eine Methode anstelle einer virtuellen zu verwenden. Der Hauptgrund für die Verwendung von Virtuals besteht darin, dass Sie Mongoose veranlassen können, Virtuals in die Ausgabe von toJSON() und toObject() einzubeziehen . Aber toJSON() und toObject() sind synchron, also würden sie die asynchrone virtuelle nicht für Sie verarbeiten.

Ich bin auch hier auf einen Anwendungsfall gestoßen, und ich versuche, eine gute Lösung zu durchdenken, bin sehr offen für Ideen und stimme zu, die Setter-Semantik nicht zu brechen.

Ich habe ein Dienstprogramm geschrieben, um automatisch JSON-Patch-Sets auf Mungo-Modelle anzuwenden. Es unterstützt die automatische Population mit tiefen Pfaden: https://github.com/claytongulick/mongoose-json-patch

Die Idee ist, dass Sie in Kombination mit einigen Regeln: https://github.com/claytongulick/json-patch-rules einer 'automatischen' API mit JSON Patch ziemlich nahe kommen können.

Mein Plan war, für Fälle, in denen einfache Zuweisungen nicht funktionieren, Virtuals zu verwenden. Wenn der Patch angewendet wird, nimmt ein virtueller alles auf, was Sie wollen - dies würde es Ihrem Interface-Objekt ermöglichen, sich vom tatsächlichen Mungo-Modell / DB-Objekt zu unterscheiden.

Zum Beispiel habe ich ein User-Objekt, das einen 'Add'-Vorgang für 'payment_methods' unterstützt. Das Hinzufügen einer Zahlungsmethode ist kein direktes Hinzufügen zu einem Array - es ist ein Aufruf an den Prozessor mit einem Zahlungstoken, das Zurückholen eines Zahlungsmethodentokens, das Speichern auf andere Weise im Modell usw.

Aber ich möchte, dass das Schnittstellenmodell, das konzeptionelle Modell, mit einer JSON-Patch-Add-Operation gepatcht werden kann.

Ohne asynchrone Setter funktioniert dies nicht. Ich denke, die einzige Option besteht darin, mongoose-json-patch als Option eine Art Zuordnung zwischen Pfaden, Operationen und mongoose-Methoden akzeptieren zu lassen, es sei denn, es gibt bessere Ideen?

@claytongulick warum benötigen Sie einen asynchronen Setter anstelle von await für einen asynchronen Vorgang und dann synchron festgelegt?

@vkarpov15 Wie wäre es, einfach toObject() und toJSON() standardmäßig asynchron zu machen und toObjectSync() und toJSONSync() Funktionen einzuführen? Sync Varianten sollten async Virtuals einfach überspringen. (Ich erinnere mich, dass dieses Muster irgendwo in Mungos verwendet wird, also wäre es nicht zu seltsam.)

Mein Anwendungsfall ist ungefähr so: Ich habe ein Schema mit einem virtuellen, das find() auf einem anderen Modell ausführt (etwas komplexer als das einfache Auffüllen einer ID). Natürlich kann ich die Dinge, die ich möchte, in mein Hauptmodell mit Save/Delete-Hooks denormalisieren, aber das bringt viele Wartbarkeitskosten mit sich (und ich brauche die Leistungsvorteile in diesem speziellen Fall wirklich nicht). Daher fühlt es sich natürlich an, eine virtuelle Person zu haben, die das für mich tut.

JSON.stringify() unterstützt async toJSON() , daher funktioniert die Idee mit toJSONSync() leider nicht.

Ich weiß, Sie sagten, dass Ihr find() ziemlich komplex ist, aber Sie sollten sich für alle Fälle die Virtuals-Bevölkerung ansehen . Sie können auch Abfrage-Middleware ausprobieren.

Hat Ihr asynchrones virtuelles auch einen Setter oder nur einen Getter

Eine Lösung für diejenigen, die dieses Problem haben:

Für den Fall, dass nur der Setter asynchron ist, habe ich eine Lösung gefunden. Es ist ein bisschen schmutzig, aber es scheint gut zu funktionieren.
Die Idee besteht darin, dem virtuellen Setter ein Objekt zu übergeben, das einen Promise-Resolver als Callback-Requisite und die zu setzende virtuelle Eigenschaft enthält. Wenn der Setter fertig ist, ruft er den Callback auf, was nach außen bedeutet, dass das Objekt gespeichert werden kann.

Um ein einfaches Beispiel zu verwenden, das von der ersten Frage inspiriert wurde:

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();
});

Dies kann ungewollte Folgen haben, also verwenden Sie es auf eigene Gefahr.

@silto warum verwenden Sie nicht einfach eine

@vkarpov15 Normalerweise würde ich das tun, aber in dem Projekt, in dem ich dies getan habe, habe ich Schemas, Virtuals und graphQL-Endpunkte automatisch aus einem Json-"Plan" generiert, daher bevorzuge ich eine einheitliche virtuelle Schnittstelle anstelle einer Methode für einen bestimmten Fall.

@silto können Sie

Im Fall von Setter möchten Sie es vielleicht speichern oder einfach nur nach den Dokumentdaten suchen. Wenn Sie speichern, ist das ein Versprechen, sodass Sie nach den Feldern suchen, die Versprechen sind, diese auflösen und dann speichern können.

Wenn Sie nach den Daten suchen möchten, können Sie durch Schemaoptionen definieren, dass dieses Modell ein Promise-Typ ist, oder wenn Sie das Modell erstellen, überprüfen Sie das Schema und prüfen Sie, ob es einen Setter, Getter oder Virtual gibt, der ein Promise ist, und drehen Sie dann um es in ein Versprechen.

Oder Sie können einfach eine exec-ähnliche Funktion verwenden, die Sie bereits haben (execPopulate).

Wenn Sie im Lebenslauf die Daten mit einem Setter, Getter oder Virtual beobachten möchten, können Sie eine entsprechende Funktion erstellen. Wenn Sie die Daten speichern möchten, ist dies bereits ein Versprechen, sodass Sie die gleiche Funktion verwenden können, um die Daten zu transformieren bevor Sie es speichern.

Ich verwende Virtuals mit Versprechen, aber da ich Express-Promise verwende, sind mir die Versprechen fast immer egal, aber in einigen Fällen verwende ich Doc..then(), da ich noch nie Setter mit Versprechen verwendet habe, habe ich dieses Problem nicht ...

In jedem Fall wäre es schön, eine Art Wrapper zu haben, um alle Daten bereits aufgelöst zu bekommen und nicht das "Dann" bei jedem versprochenen virtuellen und Getter verwenden zu müssen, oder nachdem ein versprochener Setter definiert wurde.

Wenn Sie möchten, kann ich Ihnen bei diesem Ansatz helfen.

Mit freundlichen Grüßen.

PS: ein typisches Beispiel für die Verwendung von versprochenen Virtuals, in meinem Fall verwende ich 2 Pfade, um zu wissen, ob meine Dokumente gelöscht oder anhand externer Daten aktualisiert werden können, daher muss ich normalerweise andere Modelle abfragen, um zu wissen, ob diese gelöscht oder geändert werden können . Wie ich schon sagte, Express-Versprechen lösen dieses Problem für mich, aber wenn ich intern überprüfen möchte, ob diese Aufgaben erledigt werden können, musste ich dieses Versprechen vorher lösen.

@chumager können Sie bitte einige Codebeispiele bereitstellen?

Hallo, zum Beispiel verwende ich gemäß meinem Kommentar unten 2 Virtuals _update und _delete und ein Plugin, das diese Virtuals definiert, falls es nicht im Schema definiert ist und true zurückgibt.

Ich habe ein Simulationsmodell, um einen Kredit zu definieren, und einen Projektmodus, um die Simulation mit mkt-Daten zu veröffentlichen.
Die Simulation kann nicht gelöscht werden, wenn der Simulation ein Projekt zugeordnet ist, und kann nicht aktualisiert werden, wenn das Projekt für Investitionen veröffentlicht wird.

Die Auflösung des virtuellen _update in der Simulation besteht darin, ein Projekt mit referenzierter Simulation und dem Status "En Financiamiento" zu finden. Wenn diese Abfrage wahr ist, kann die Simulation nicht aktualisiert werden ... offensichtlich ist "find" a versprochen, also das virtuelle ist es auch...

Da ich diese Virtualität normalerweise im Frontend verwende, werden die Daten von einem Modul geparst, das das Objekt auflöst (co oder express-promise abhängig ist eines oder ein Array von Ergebnissen).

Für den Fall, dass ich das Dokument sehen wollte, werde ich feststellen, dass meine virtuellen Versprechungen sind, also muss ich zum Auflösen das co-Modul verwenden, aber ich musste bereits result als versprechen verwenden ... vielleicht füge ich nur co zum ergebnis hinzu wird die Magie tun, oder mit einem Plugin, das co after find verwendet ... aber es scheint natürlicher zu sein, dass die Ergebnismenge die Arbeit bereits erledigt hat.

Ich verwende viele Endpunkte, um Daten von Mungos zu erhalten, ich muss diese Funktion überall verwenden oder einen Post-Hook für die Suche verwenden.

Das Gleiche gilt für Getter, mit Settern sollte der Hook vor der Validierung sein, aber es ist wichtig, andere Requisiten aus dem Dokument nicht zu berühren, da es Zirkelverweise und andere Requisiten wie den Konstruktor hat.

Grüße...

PS: Wenn Sie wirklich Beispielcodo benötigen, lassen Sie es mich bitte wissen.

@chumager große

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen