Mongoose: nueva característica: virtual async

Creado en 27 oct. 2017  ·  42Comentarios  ·  Fuente: Automattic/mongoose

Nueva función virtual async , ¡por favor soporte!

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

Comentario más útil

Algunas muy buenas ideas en su código, hemos visto este problema varias veces, pero no hemos tenido tiempo de investigarlo. Sin embargo, me gusta esta idea, la consideraré para un próximo lanzamiento.

Todos 42 comentarios

Actual, uso una forma sucia:

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

Algunas muy buenas ideas en su código, hemos visto este problema varias veces, pero no hemos tenido tiempo de investigarlo. Sin embargo, me gusta esta idea, la consideraré para un próximo lanzamiento.

el problema es ... ¿cómo es la sintaxis de uso?

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

Esto no funciona.

Según ECMA262 12.15.4 , el valor de retorno de user.password = 'some-secure-password' debe ser _rval_, que en este caso es 'some-secure-password' .

Usted propone que el valor de retorno de someVar = object sea ​​un Promise , y de acuerdo con este hilo y la especificación ES262 vinculada anteriormente, es una "violación profunda de la semántica de ES".

Además, intentar implementar un problema de violación semántica de este tipo con el mero propósito de tener una función de conveniencia es una idea bastante mala, especialmente porque podría significar todo tipo de cosas malas para el código base de mangosta en su conjunto.

¿Por qué no lo haces?

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

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

Literalmente, no hay necesidad de intentar hacer un setter async , lo que no debería hacerse en primer lugar, para una frase tan simple como esta.

También puedes hacer esto:

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

No tengo idea de por qué te tomarías la molestia de hacer virtuales, ganchos de pre-guardado, etc.

Estoy de acuerdo con @heisian. En mi humilde opinión, esto se siente como una función / api inflada. No veo cómo la alternativa de usar un método de instancia sea inconveniente aquí. Pero agregar un soporte de sintaxis bastante importante para esto definitivamente se siente hinchado.

Deberíamos tener una característica muy simple como esta:

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

@gcanu , está ignorando por completo lo que

¿Qué hay de malo en simplemente hacer?

await User.setPassword('password');

???

En caso de que no lo hayas visto antes, esto no funcionará :

await (User.password = 'password');

@ vkarpov15 Este no es un problema específico de la mangosta, sino que cuestiona la validez de la especificación actual de ECMAScript. Esta "solicitud de función" debería cerrarse ...

¡El siguiente código es una muy mala idea! ¿Por qué establecer una contraseña incluye la operación 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...');

Mangosta necesita más moderno, más elegante.

@heisian Ok, mi error, no me

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

Actualmente, en Mongoose IMPL, getter o setter solo registra una función y luego llama, no es https://tc39.github.io/ecma262/#sec -assignment-operator-runtime-semantics -evaluación y https://github.com/tc39/ecmascript-asyncawait/issues/82. Eso es diferente.

Así que por favor abra esta solicitud.

@fundon , dime esto: ¿cómo llamarás exactamente a tu montador virtual? Muestra el uso. Si está usando async , debe manejarse mediante una promesa. Su ejemplo original no muestra await en ninguna parte de la llamada de asignación / establecimiento.

Mi código de ejemplo es solo un ejemplo ... también puede hacer esto tan fácilmente:

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

Obviamente..

Tu ejemplo no es un buen camino para mí.

quiero

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

Hash la contraseña en el modo más simple, más elegante.

¿Por qué? para que pueda guardar algunas líneas de código? la razón no es suficiente para justificar todo el trabajo adicional y posiblemente cambios importantes en la base de código.

También debes darte cuenta al final del día, sin importar cómo lo expreses, lo que está sucediendo internamente dentro de Mongoose es un setter, que no puede ser asincrónico / en espera.

No estoy de acuerdo con @heisian. Mangoose tiene demasiadas cosas viejas. ¡Mangosta necesita refactorización!
Mangosta necesita moderno.

Si este problema está cerrado. ¡Tendré Mongoose, lo refactorizaré! ¡Chau!

¡Excelente! Ese es el punto del código abierto. Continúe y cree una horquilla con una versión recortada, sería bueno para todos nosotros.

Realmente no hay preocupación por la necesidad de await (User.password = 'password'); . El único inconveniente real es que user.password = 'password'; significará que hay una operación asincrónica que está sucediendo, por lo que user.passwordSalt no se configurará. La forma en que eso se relaciona con los ganchos también es una pregunta interesante: ¿qué sucede si tiene un gancho pre('validate') o pre('save') Deberían esperar hasta que finalice la operación asíncrona user.password ?

No me siento inclinado a descartar este problema de plano. Hay mucho valor en consolidar el comportamiento asincrónico detrás de .save() , .find() , etc., solo hay que asegurarse de que se ajuste perfectamente al resto de la API.

Hoy en día, los captadores y definidores asíncronos son muy importantes para mí. Necesito enviar solicitudes HTTP de captadores y establecedores para descifrar / cifrar campos con métodos de cifrado patentados, y actualmente, no hay forma de hacerlo. ¿Tienes una idea de cómo lograrlo?

@gcanu Simplemente implementaría esos como métodos

Por las razones mencionadas y el hecho de que existen métodos para manejar fácilmente cualquier operación asíncrona que necesite, no veo ninguna utilidad detrás de la consolidación del comportamiento de async ... nuevamente, await (User.password = 'password') rompe la convención de ECMAScript y yo Garantizar que será difícil y que no vale la pena implementarlo con elegancia ...

Tienes razón, ese patrón no es algo que implementaremos. La idea de esperar a que se resuelva el virtual asincrónico antes de guardar es interesante.

Me encantaría una implementación toJSON({virtuals: true}) . Algunos de los campos virtuales los obtengo al ejecutar otras consultas en la base de datos, que solo quiero ejecutar una vez que serialice.

@gabzim eso sería bastante complicado porque JSON.stringify no admite promesas. Por lo tanto, res.json () nunca podrá manejar virtuales asíncronos a menos que agregue ayudantes adicionales para expresar.

Ah sí, tiene sentido, gracias @ vkarpov15

¿Sería una buena práctica realizar una consulta dentro de get callback?
Creo que esto sería útil en algunos casos.

Digamos que quiero obtener la ruta completa de una página web (o documento), donde los documentos se pueden anidar, algo así como las rutas URL de 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 }`
})

Aquí tenemos que usar async / await ya que las operaciones de consulta son asincrónicas.

@JulianSoto en este caso, te recomiendo que uses un método en lugar de uno virtual. La razón principal para usar virtuales es porque puede hacer que Mongoose incluya virtuales en la salida toJSON() y toObject() . Pero toJSON() y toObject() son síncronos, por lo que no manejarían el virtual asíncrono por usted.

También me encontré con un caso de uso para esto, y estoy tratando de pensar en una buena solución, muy abierto a las ideas y estoy de acuerdo con no romper la semántica del setter.

Escribí una utilidad para aplicar automáticamente conjuntos de parches JSON a modelos de mangosta. Admite la población automática con rutas profundas: https://github.com/claytongulick/mongoose-json-patch

La idea es que, en combinación con algunas reglas: https://github.com/claytongulick/json-patch-rules , puedes acercarte bastante a tener una API 'automática' con JSON Patch.

Mi plan ha sido que, para los casos en los que la asignación simple no funcione, usar virtuales. Cuando se aplica el parche, un virtual recogerá todo lo que desee; esto permitiría que su objeto de interfaz sea diferente del modelo de mangosta / objeto de base de datos real.

Por ejemplo, tengo un objeto Usuario que admite una operación 'agregar' en 'métodos_pago'. Agregar un método de pago no es una adición directa a una matriz: es una llamada al procesador con un token de pago, recuperar un token de método de pago, almacenarlo de una manera diferente en el modelo, etc.

Pero me gustaría que el modelo de interfaz, el modelo conceptual, se pueda parchear con un parche JSON 'add' op.

Sin establecedores asíncronos, esto no funcionará. Supongo que la única opción es hacer que mongoose-json-patch acepte como opción algún tipo de mapeo entre rutas, operaciones y métodos de mangosta, a menos que haya mejores ideas.

@claytongulick ¿por qué necesita un await en una operación asíncrona y luego configurarlo sincrónicamente?

@ vkarpov15 ¿Qué tal simplemente hacer toObject() y toJSON() asíncronos por defecto e introducir las funciones toObjectSync() y toJSONSync() ? Sync variantes async virtuales. (Recuerdo que este patrón se usa en mangostas en algún lugar, por lo que no sería demasiado extraño tenerlo).

Mi caso de uso es algo como esto: tengo un esquema que tiene un virtual que hace un find() en otro modelo (un poco más complejo que simplemente completar una identificación). Por supuesto, puedo desnormalizar las cosas que quiero en mi modelo principal usando ganchos para guardar / eliminar, pero eso conlleva muchos costos de mantenimiento (y realmente no necesito los beneficios de rendimiento en este caso particular). Así que se siente natural tener un virtual que haga eso por mí.

JSON.stringify() no admite async toJSON() , por lo que, lamentablemente, la idea toJSONSync() no funcionará.

Sé que dijiste que tu find() es bastante complejo, pero es posible que desees echar un vistazo a rellenar virtuales por si acaso. También puede probar el middleware de consultas.

Además, ¿su async virtual tiene un setter o solo un getter @isamert ?

Una solución para quienes tienen este problema:

En el caso de que solo el setter sea asincrónico, he encontrado una solución. Está un poco sucio pero parece funcionar bien.
La idea es pasar al configurador virtual un objeto que contenga un resolutor de promesas como apoyo de devolución de llamada y la propiedad virtual para configurar. cuando el setter termina, llama a la devolución de llamada, lo que significa para el exterior que el objeto se puede guardar.

Para usar un ejemplo básico inspirado en la primera pregunta:

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

Esto podría tener consecuencias no deseadas, así que utilícelo bajo su propio riesgo.

@silto, ¿por qué no usas un método de esquema que devuelve una promesa?

@ vkarpov15 Normalmente haría eso, pero en el proyecto donde hice esto tengo esquemas, virtuales y puntos finales graphQL generados automáticamente a partir de un "plan" json, así que prefiero tener una interfaz virtual uniforme en lugar de un método para un caso particular.

@silto ¿ puede proporcionar ejemplos de código? Me encantaría ver cómo se ve esto

En el caso de setter, es posible que desee guardarlo o simplemente buscar los datos del documento, si lo guarda, es una promesa, para que pueda verificar los campos que son promesas, resolverlos y luego guardar.

Si desea buscar los datos, puede definir por opciones de esquema que este Modelo será un tipo Promesa o cuando cree el modelo verifique el esquema y verifique si hay un establecedor, captador o virtual que sea una promesa y luego gire en una promesa.

O simplemente puede usar una función similar a la de ejecutivo que ya tiene (execPopulate).

en resumen, si quieres observar los datos que tiene un setter, getter o virtual puedes construir una función para eso importa, si quieres guardar los datos ya es una promesa por lo que puedes usar la misma función para transformar los datos antes de guardarlo.

Solía ​​usar virtuales con promesas, pero como uso la promesa expresa, casi todo el tiempo no me importan las promesas, pero en algunos casos uso Doc.. then (), como nunca he usado setters con promesas, no tengo ese problema ...

De cualquier manera, sería bueno tener algún tipo de envoltorio para obtener todos los datos ya resueltos y no tener que usar el "entonces" en cada virtual y getter prometido, o después de definir un establecedor prometido.

Si quieres puedo ayudarte con este enfoque.

Atentamente.

PD: un ejemplo típico de uso de virtuales promisifados, en mi caso uso 2 rutas para saber si mis Documentos se pueden eliminar o actualizar según datos externos, por lo que generalmente necesito consultar otros modelos para saber si este se puede eliminar o modificar. . Como dije antes, la promesa expresa resuelve este problema por mí, pero si quiero verificar internamente si esas tareas se pueden hacer, tuve que resolver estas promesas antes.

@chumager , ¿puede proporcionar algunos ejemplos de código?

Hola, por ejemplo, de acuerdo con mi comentario a continuación, uso 2 virtuales _update y _delete, y un complemento que define esos virtuales en caso de que no esté definido en el esquema, devolviendo verdadero.

Tengo un modelo de simulación para definir un crédito y un modo de proyecto para publicar la simulación con datos mkt.
La simulación no se puede eliminar en caso de que haya un proyecto asociado con la simulación y no se puede actualizar si el proyecto se publica para inversiones.

La resolución del _update virtual en la simulación es encontrando un proyecto con la simulación referenciada y el estado es "En Financiamiento", si esta consulta es verdadera entonces la simulación no se puede actualizar ... obviamente el "buscar" es un lo prometo, así que lo virtual también es ...

Como normalmente uso este virtual en el frontend, los datos son analizados por un módulo que resuelve el objeto (co o express-promise dependiendo de uno o una matriz de resultado).

En el caso de que quisiera ver el documento, encontraré que mis virtuales son promesas, así que tengo que usar el módulo co para resolver, pero ya tuve que usar el resultado como promesa ... tal vez solo agregue co al resultado hará la magia, o con un complemento que use co después de encontrar ... pero parece más natural que el conjunto de resultados ya haya hecho el trabajo.

Utilizo muchos puntos finales para obtener datos de mangosta, si tendré que usar esa función en todas partes o usar un gancho de publicación para buscar.

Lo mismo ocurre con los getters, con los setters el gancho debe ser una validación previa, pero es importante no tocar otros accesorios del documento, ya que tiene referencias circulares y otros accesorios como el constructor.

Saludos...

PD: Si realmente necesitas un ejemplo de codo, házmelo saber.

@chumager ¡ gran muro de prosa! == muestra de código. Realmente preferiría una muestra de código.

¿Fue útil esta página
0 / 5 - 0 calificaciones