Mongoose: novo recurso: assíncrono virtual

Criado em 27 out. 2017  ·  42Comentários  ·  Fonte: Automattic/mongoose

Novo recurso virtual async , suporte por favor!

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

Comentários muito úteis

Algumas ideias muito boas em seu código, vimos esse problema algumas vezes, mas não tivemos tempo de investigá-lo. Eu gosto dessa ideia, porém, considerarei para um próximo lançamento

Todos 42 comentários

Atual, eu uso uma forma suja:

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

Algumas ideias muito boas em seu código, vimos esse problema algumas vezes, mas não tivemos tempo de investigá-lo. Eu gosto dessa ideia, porém, considerarei para um próximo lançamento

o problema é ... como é a sintaxe de uso?

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

Isso não funciona.

De acordo com ECMA262 12.15.4 , o valor de retorno de user.password = 'some-secure-password' deve ser _rval_, que neste caso é 'some-secure-password' .

Você está propondo que o valor de retorno de someVar = object seja Promise e, de acordo com este tópico e a especificação ES262 vinculada acima, é uma "violação profunda da semântica ES".

Além disso, tentar implementar esse problema de violação da semântica com o mero propósito de ter uma função de conveniência é uma ideia muito ruim, especialmente porque pode significar todos os tipos de coisas ruins para a base de código do mangusto como um todo.

Por que você simplesmente não faz:

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

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

Não há literalmente necessidade de tentar fazer um setter async , o que não deveria ser feito em primeiro lugar, para uma linha simples como esta.

Você também pode simplesmente fazer isso:

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

Não tenho ideia de por que você teria todo o trabalho de fazer virtuais, pré-salvar ganchos, etc ...

Eu concordo com @heisian. Isso parece um bloat de recurso / API para mim, IMHO. Não vejo como a alternativa de usar um método de instância seja inconveniente aqui. Mas adicionar um suporte de sintaxe muito importante para isso definitivamente parece um inchaço.

Devemos ter um recurso muito simples como este:

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

@gcanu você está ignorando completamente o que eu postei, o que você está propondo retorna uma promessa de uma chamada de atribuição e que quebra completamente a especificação Javascript / ECMA262. Para que seu trecho de código funcione, sua função setter precisa ser uma promessa, o que por definição não é permitido por especificação e não funcionaria de qualquer maneira.

O que há de errado em apenas fazer:

await User.setPassword('password');

???

Caso você não tenha visto antes, isso não funcionará :

await (User.password = 'password');

@ vkarpov15 Este não é um problema específico do mangusto, mas sim um questionamento da validade da especificação ECMAScript atual. Esta "solicitação de recurso" deve ser fechada ...

O código abaixo é uma ideia muito ruim! Por que definir a senha inclui a operação 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...');

O Mongoose precisa de mais moderno, mais elegante.

@heisian Ok meu erro, eu não cuidei do uso do setter ...

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

Atualmente, no Mongoose IMPL, getter ou setter apenas registra uma função e a chama, não é https://tc39.github.io/ecma262/#sec -assignment-operator-runtime-semantics -avaliação e https://github.com/tc39/ecmascript-asyncawait/issues/82. Isso é diferente.

Então, por favor, abra este pedido.

@fundon , diga-me o seguinte: como exatamente você chamará seu async , deve ser tratado por uma promessa. Seu exemplo original não mostra await em qualquer lugar na chamada setter / atribuição.

Meu código de exemplo é apenas um exemplo ... você também pode fazer isso facilmente:

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..

Seu exemplo não é um bom caminho para mim.

eu quero

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

Hash a senha no modo que mais simples, mais elegante.

porque? para que você possa economizar algumas linhas de código? a razão não é suficiente para justificar todo o trabalho extra e possivelmente interromper as alterações na base de código.

Você também deve perceber no final do dia, não importa como você as expresse, o que está acontecendo internamente no Mongoose é um setter, que não pode ser assíncrono / esperar.

Não concordo com @heisian. O Mongoose tem muitas coisas velhas. O Mongoose precisa de refatoração!
O Mongoose precisa de modernidade.

Se este problema for encerrado. Vou fazer um fork do Mongoose, refatorá-lo! Tchau!

Excelente! Esse é o ponto do código aberto. Vá em frente e crie um garfo com uma versão reduzida, seria bom para todos nós.

Não há realmente nenhuma preocupação em precisar de await (User.password = 'password'); . A única desvantagem real é que user.password = 'password'; significará que há alguma operação assíncrona acontecendo, portanto user.passwordSalt não será definido. Como isso se relaciona com ganchos também é uma questão interessante: o que acontece se você tiver um gancho pre('validate') ou pre('save') , eles devem esperar até que a operação assíncrona user.password seja concluída?

Não estou inclinado a descartar esse problema imediatamente. A consolidação do comportamento assíncrono por trás de .save() , .find() , etc. pode ser muito valiosa, só é preciso ter certeza de que ele se encaixa perfeitamente no restante da API.

Hoje, getters e setters assíncronos são muito importantes para mim. Preciso enviar solicitações HTTP de getters e setters para descriptografar / criptografar campos com métodos de criptografia proprietários e, atualmente, não há como fazer isso. Você tem uma ideia de como fazer isso?

@gcanu eu apenas implementaria esses como métodos

Pelas minhas razões mencionadas e pelo fato de que há métodos para lidar facilmente com qualquer operação assíncrona de que você precisa, não vejo nenhuma utilidade por trás da consolidação do comportamento async .. novamente, await (User.password = 'password') quebra a convenção ECMAScript e eu garanto que vai ser difícil e não vale a pena implementar com elegância ...

Você está certo, esse padrão não é algo que iremos implementar. A ideia de esperar que o virtual assíncrono resolva antes de salvar é interessante.

Eu adoraria uma implementação de toJSON({virtuals: true}) . Alguns dos campos virtuais que obtenho executando outras consultas ao banco de dados, que desejo executar apenas depois de serializar.

@gabzim isso seria muito confuso porque JSON.stringify não oferece suporte a promessas. Portanto, res.json () nunca será capaz de lidar com virtuais assíncronos, a menos que você adicione ajudantes extras para expressar.

Sim, faz sentido, obrigado @ vkarpov15

Seria uma boa prática fazer uma consulta dentro de get callback?
Acho que isso seria útil em alguns casos.

Digamos que eu queira obter o caminho completo de uma página da web (ou documento), onde os documentos podem ser aninhados, algo como caminhos de URL do 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 }`
})

Aqui, temos que usar async / await, pois as operações de consulta são assíncronas.

@JulianSoto neste caso, recomendo que você use um método ao invés de um virtual. A principal razão para usar virtuais é porque você pode fazer o Mongoose incluir virtuais na saída toJSON() e toObject() . Mas toJSON() e toObject() são síncronos, então eles não manipulariam o virtual assíncrono para você.

Também encontrei um caso de uso para isso e estou tentando pensar em uma boa solução, muito aberto a ideias e concordo em não quebrar a semântica do setter.

Eu escrevi um utilitário para aplicar automaticamente conjuntos de patch JSON a modelos de mangusto. Ele oferece suporte ao preenchimento automático com caminhos profundos: https://github.com/claytongulick/mongoose-json-patch

A ideia é que, em combinação com algumas regras: https://github.com/claytongulick/json-patch-rules, você pode chegar muito perto de ter uma API 'automática' com JSON Patch.

Meu plano é que, para os casos em que a atribuição simples não funcione, use os virtuais. Quando o patch é aplicado, um virtual pegará qualquer coisa que você quiser - isso permitiria que seu objeto de interface fosse diferente do modelo / objeto db real do mangusto.

Por exemplo, eu tenho um objeto User que suporta uma operação 'add' em 'payment_methods'. Adicionar um método de pagamento não é uma adição direta a um array - é uma chamada para o processador com um token de pagamento, recebendo um token de método de pagamento de volta, armazenando-o de uma maneira diferente no modelo, etc ...

Mas eu gostaria que o modelo de interface, o modelo conceitual, pudesse ser corrigido com um patch JSON 'add' op.

Sem configuradores assíncronos, isso não funcionará. Eu acho que a única opção é fazer com que mongoose-json-patch aceite como uma opção algum tipo de mapeamento entre caminhos, ops e métodos de mangusto, a menos que haja ideias melhores.

@claytongulick por que você precisa de um await em uma operação assíncrona e, em seguida, configurado de forma síncrona?

@ vkarpov15 Que tal simplesmente tornar toObject() e toJSON() assíncronos por padrão e introduzir funções toObjectSync() e toJSONSync() ? Sync variantes devem simplesmente pular async virtuais. (Lembro que esse padrão é usado em algum lugar do mangusto, então não seria muito estranho ter.)

Meu caso de uso é mais ou menos assim: tenho um esquema que tem um virtual que faz find() em outro modelo (um pouco mais complexo do que simplesmente preencher um id). Claro que posso desnormalizar as coisas que quero em meu modelo principal usando ganchos salvar / excluir, mas isso vem com muitos custos de manutenção (e eu realmente não preciso dos benefícios de desempenho neste caso específico). Portanto, parece natural ter um virtual para fazer isso por mim.

JSON.stringify() não suporta toJSON() assíncrono , então, infelizmente, a ideia toJSONSync() não funcionará.

Eu sei que você disse que seu find() é muito complexo, mas você pode querer dar uma olhada em popular os virtuais apenas para

Além disso, seu async virtual tem um setter ou apenas um getter @isamert ?

Uma solução para quem tem esse problema:

No caso em que apenas o setter é assíncrono, encontrei uma solução. Está um pouco sujo, mas parece funcionar bem.
A ideia é passar para o configurador virtual um objeto contendo um resolvedor de promessas como um suporte de retorno de chamada e a propriedade virtual a ser configurada. quando o setter termina, ele chama o callback, o que significa para o exterior que o objeto pode ser salvo.

Para usar um exemplo básico inspirado na primeira pergunta:

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

Isso pode ter consequências indesejáveis, portanto, use por sua própria conta e risco.

@silto, por que você simplesmente não usa um método de esquema que retorna uma promessa?

@ vkarpov15 Eu normalmente faria isso, mas no projeto em que fiz isso, tenho esquemas, virtuais e endpoints de graphQL gerados automaticamente a partir de um "plano" json, então prefiro ter uma interface virtual uniforme em vez de um método para um caso específico.

@silto, você pode fornecer algum exemplo de código? Eu adoraria ver como é isso

No caso do setter, você pode querer salvá-lo ou apenas procurar os dados do documento, se salvar, fica uma promessa, então você pode verificar os campos que são promessas, resolvê-los e depois salvar.

Se você quiser procurar os dados, você pode definir por opções de esquema que este modelo será um tipo de promessa ou quando você criar o modelo verifique o esquema e verifique se há um setter, getter ou virtual que seja uma promessa e depois ligue em uma promessa.

Ou você pode simplesmente usar uma função semelhante ao exec que você já possui (execPopulate).

em resumo, se você deseja observar os dados que possuem um setter, getter ou virtual você pode construir uma função para isso importa, se você deseja salvar os dados já é uma promessa, então você pode usar a mesma função para transformar os dados antes de salvá-lo.

Costumo usar virtuais com promessas mas como uso express-prometem, quase sempre não ligo para as promessas mas em alguns casos uso o Doc..então (), como nunca usei setters com promessas não tenho esse problema ...

De qualquer forma, seria bom ter algum tipo de wrapper para obter todos os dados já resolvidos e não precisar usar o "then" em cada getter e virtual prometido, ou depois de definir um setter prometido.

Se você quiser, posso ajudá-lo com essa abordagem.

Cumprimentos.

PS: um exemplo típico de uso de virtuais prometidos, no meu caso utilizo 2 caminhos para saber se meus Documentos podem ser deletados ou se atualizam de acordo com dados externos, então normalmente preciso consultar outros modelos para saber se este pode ser deletado ou modificado . Como eu disse antes, promessa expressa resolverá esse problema para mim, mas se eu quiser verificar internamente se essas tarefas podem ser feitas eu tinha que resolver essa promessa antes.

@chumager pode fornecer alguns exemplos de código?

Olá, por exemplo, de acordo com meu comentário abaixo, utilizo 2 virtuais _update e _delete, e um plugin que define esses virtuais caso não esteja definido no esquema, retornando true.

Eu tenho um modelo de simulação para definir um crédito e um modo de projeto para publicar a simulação com dados mkt.
A simulação não pode ser excluída caso haja um projeto associado à simulação, e não pode ser atualizada se o projeto for publicado para investimentos.

A resolução da _atualização virtual na simulação é encontrar um projeto com a simulação referenciada e o status é "En Financiamiento", se esta consulta for verdadeira, então a simulação não pode; t ser atualizada ... obviamente, o "encontrar" é um promessa, então o virtual também é ...

Como normalmente eu uso esse virtual no frontend, os dados são analisados ​​por um módulo que resolve o objeto (co ou expressa-promessa dependendo de um ou de um array de resultado).

No caso de eu querer ver o documento, vou descobrir que meus virtuais são promessas, então tenho que usar o módulo co para resolver, mas já tive que usar resultado como promessa ... talvez apenas adicionando co ao resultado vai fazer a mágica, ou com um plugin que usa co depois de encontrar ... mas parece mais naturalmente o conjunto de resultados já fez o trabalho.

Eu uso muitos endpoints para obter dados do mongoose, mas terei que usar essa função em todos os lugares ou usar um post hook para localizar.

mesma coisa com getters, com setters o gancho deve ser pré-validação, mas é importante não mexer em outros adereços do documento, pois ele tem referências circulares e outros adereços como o construtor.

Cumprimentos...

PS: Se você realmente precisa de codo de exemplo, por favor me avise.

@chumager grande parede de prosa! == amostra de código. Eu realmente preferiria um exemplo de código.

Esta página foi útil?
0 / 5 - 0 avaliações