Gravando dados com travajs



No meu post anterior, descrevi os pontos principais ao desenvolver outra biblioteca de código-fonte aberto . Esqueci de mencionar mais uma coisa: se você não contar a ninguém sobre a biblioteca, seja ela qual for, provavelmente ninguém saberá sobre isso.

Portanto, conheça trava.js - validação suculenta para o benefício do projeto. A propósito, usamos grama há mais de seis meses e pensei que era hora de falar sobre os benefícios de usá-la. Já está seco, então prenda a respiração. E vá em frente.

Conceito


À primeira vista, parece que a validação é um tópico trivial que não requer atenção especial. O valor é verdadeiro ou não, o que poderia ser mais simples:

function validate (value) { // any checking... if (!check(value)) return false; return true; } 

Mas geralmente seria bom saber o que exatamente deu errado:

 function validate (value) { if (!check1(value)) return 'ERROR_1'; if (!check2(value)) return 'ERROR_2'; } 

Na verdade, é tudo, o problema está resolvido.

Se não fosse por um "mas".

A partir da experiência no desenvolvimento de aplicativos reais, percebeu-se que o assunto não termina com a validação. Geralmente, esses dados também precisam ser convertidos para um formato específico, por algum motivo não suportado pelo serializador pronto para uso, por exemplo, datas, conjuntos ou outros tipos de dados personalizados. Dado que isso é principalmente JSON, na prática, é necessário fazer uma passagem dupla pela estrutura de dados de entrada durante a validação e transformação. A idéia surgiu, por que não combinar esses dois estágios em um. Uma possível vantagem também seria a presença de um esquema de dados declarativo explícito.

Para suportar a conversão de um valor para um formato específico, o validador deve poder retornar não apenas um erro, mas também um valor reduzido. No mundo js, ​​várias opções de interface são bastante comuns com possíveis retornos de erro.

  1. Provavelmente o mais comum é o retorno da tupla [error, data]:

     function validate (value) { if (!check1(value)) return ['ERROR_1']; if (!check2(value)) return ['ERROR_2']; return [null, value]; } 

    Também existe uma opção semelhante em que não é retornada uma matriz, mas o objeto {error, data} , mas não há diferenças fundamentais. A vantagem dessa abordagem é a obviedade, o menos é que agora em todos os lugares é necessário manter esse contrato. Para validação, isso não causa transtornos, mas para transformações isso é claramente supérfluo.
  2. Use exceções. Embora, na minha opinião, um erro de validação seja uma situação padrão no aplicativo, nada é excepcional. Honestamente, acho que as exceções são melhor usadas apenas quando algo realmente deu errado. Além disso, as exceções podem ser chamadas acidentalmente nos próprios validadores e, talvez, você não saiba que houve um erro no código e não no valor. A vantagem da abordagem é a simplificação da interface - agora sempre o valor é retornado da maneira usual e o erro é lançado como uma exceção.
  3. Existe uma opção para colocar um erro em uma variável global. Mas eu não puxaria o estado desnecessariamente.
  4. Use um tipo separado para erros. Parece ser a opção com exceções, se você receber o tipo de erro, mas não jogá-lo fora.

     function validate (value) { if (!check1(value)) return new Trava.ValidationError({ code: 401 }); if (!check2(value)) return new Trava.ValidationError({ code: 405 }); return parseOrTransform(value); // apply some parse or transform } 

Optei pela última opção, embora isso também seja um compromisso, mas no geral não é ruim. O Trava.ValidationError é proposto como um tipo para o erro, que herda do erro padrão e adiciona a capacidade de usar um tipo de dados arbitrário para relatar um erro. Não é necessário usar o Trava.ValidationError , você pode usar o erro padrão, mas não esqueça que a mensagem de erro é apenas de cadeias.

Para resumir, podemos dizer que o validador é uma função síncrona e limpa que, além do valor, pode retornar um erro. Extremamente simples. E essa teoria funciona bem sem bibliotecas. Na prática, os validadores são combinados em cadeias e hierarquias, e aqui a grama definitivamente será útil.

Composição:


Talvez a composição seja o caso mais comum de trabalho com validadores. A implementação da composição pode ser diferente. Por exemplo, nas famosas bibliotecas joi e v8n, isso é feito por meio de um objeto e uma cadeia de métodos:

 Joi.string().alphanum().min(0).max(255) 

Embora pareça bonito à primeira vista, essa abordagem tem várias desvantagens, e uma é fatal. E aqui está a coisa. Na minha experiência, um validador é sempre uma coisa para uma aplicação específica; portanto, o foco principal da biblioteca deve ser a conveniência de expandir validadores e a integração com a abordagem existente, e não o número de primitivas básicas, que, na minha opinião, apenas adicionam peso à biblioteca, mas a maioria não será usada. Tome, por exemplo, o mesmo validador para a sequência. Acontece que você precisa aparar os espaços a partir das extremidades; de repente, você precisa permitir o uso de caracteres especiais em um único caso, e em algum lugar que você precise levar a letras minúsculas, etc. De fato, pode haver infinitas dessas primitivas, e não vejo o ponto de começar a adicioná-las à biblioteca. Na minha opinião, o uso de objetos também é redundante e leva a um aumento de complexidade durante a expansão, embora à primeira vista pareça facilitar a vida. Por exemplo, c joi não é tão fácil de escrever seu validador .

Uma abordagem funcional e grama aqui pode ajudar. O mesmo exemplo de validação de um número especificado no intervalo de 0 a 255:

 //    const isNumber = n => typeof n == 'number' && !isNaN(n); //  const numberValidator = Trava.Check(isNumber); const byteValidator = Trava.Compose([ numberValidator, Trava.Check(n => 0 <= n && n < 256), ]); byteValidator(-1); // ! 

A instrução Check transforma um validador na verificação da verdade (valor => verdadeiro / falso). E Compose encadeia os validadores. Quando executada, a cadeia é interrompida após o primeiro erro. O importante é que as funções comuns sejam usadas em todos os lugares, que são muito simples de expandir e usar. É essa facilidade de expansão, na minha opinião, que é um recurso essencial de uma biblioteca de validação válida.

Tradicionalmente, um local separado na validação é ocupado verificando nulo e indefinido . Existem operadores auxiliares na grama para isso:

 //   null  undefined const requiredNumberValidator = Trava.Required(numberValidator); requiredNumberValidator(undefined); // ! const optNumberValidator = Trava.Optional(numberValidator, 2); // 2 is default optNumberValidator(undefined); // 2 optNumberValidator(null); // null const nullNumberValidator = Trava.Nullable(numberValidator, 3); // 3 is default nullNumberValidator(undefined); // 3 nullNumberValidator(null); // 3 

Existem muitos outros operadores auxiliares na grama, e todos eles se compõem de maneira bonita e surpreendentemente simplesmente se expandem. Como funções comuns :)

Hierarquia


Tipos de dados simples são organizados em uma hierarquia. Os casos mais comuns são objetos e matrizes. Existem operadores na grama que facilitam o trabalho com eles:

 //   const byteArrayValidator = Trava.Each(byteValidator); byteArrayValidator([1, -1, 2, -3]); // ValidationError: {"1":"Incorrect value","3":"Incorrect value"} //   const pointValidator = Trava.Keys({ x: numberValidator, y: numberValidator, }); pointValidator({x: 1, y: 'a'}); // ValidationError: {"y":"Incorrect value"} 

Ao validar objetos, foi decidido enfatizar a gravidade da definição: todas as chaves são necessárias por padrão (agrupadas em Necessário ). Chaves não especificadas no validador são descartadas.

Algumas soluções jsonschema , quartet preferem descrever validadores na forma de dados, por exemplo {x: 'number', y: 'number'}, mas isso leva às mesmas dificuldades ao expandir. Uma vantagem significativa dessa abordagem é a possibilidade de serialização e troca de circuitos, o que é impossível com funções. No entanto, isso pode ser facilmente implementado em cima da interface funcional. Não há necessidade de ocultar funções atrás das linhas! As funções já têm nomes e é tudo o que é necessário.

Para facilitar o uso em validadores, os operadores Compor e Chaves podem ser omitidos; também é conveniente agrupar o validador raiz no Trava :

 const pointValidator = Trava({ //  -> Keys x: [numberValidator, Trava.Check(v => v > 180)], //  -> Compose y: [numberValidator, Trava.Check(v => v < 180)], }); 

Se você chamar Trava com o segundo argumento, o valor de retorno será o resultado da aplicação do validador:

 const point = Trava({ x: [numberValidator, Trava.Check(v => v > 180)], y: [numberValidator, Trava.Check(v => v < 180)], }, //      { x: 200, y: 100, }); // { x: 200, y: 100 } 

Até agora, o suporte foi implementado apenas para matrizes e objetos, como basicamente envenenam JSON e isso é o suficiente. Solicitações Pull para Wellcome!

Contexto


Ao usar o validador como o último parâmetro, você pode transmitir o contexto que será acessível a partir de todos os validadores chamados como o último parâmetro. Pessoalmente, essa oportunidade ainda não foi útil para mim, mas é possível.

Para alguns validadores que podem retornar um erro, é possível definir uma mensagem de erro em diferentes níveis. Um exemplo:

 const pos = Trava.Check(v => v > 0); pos(-1); // ValidationError: "Incorrect value" (by default) 

Substituir por um único caso:

 const pos = Trava.Check(v => v > 0, "    "); pos(-1); // ValidationError: "    " 

Substituir para todos os casos:

 Trava.Check.ErrorMessage = " "; pos(-1); // ValidationError: " " 

Além disso, para uma configuração mais detalhada, você pode transferir uma função no local do erro, que deve retornar um erro e será chamada com os parâmetros do validador.

Caso de uso


Envenenamos principalmente o JSON no backend junto com o koa. O frontend também se senta lentamente. É conveniente ter validadores comuns nas duas extremidades. E agora vou mostrar um caso de uso quase real. Suponha que você queira implementar uma API para criar e atualizar dados do paciente.

 // validators.js const trava = require('trava'); const { isFilledString, isDate, isNumber } = require('../common/validators'); const patientSchema = { name: isFilledString, dateOfBirth: isDate, height: isNumber, } //        //      const patientNew = trava(patientSchema); //      const patientPatch = trava(mapValues(patientSchema, trava.Optional)); module.exports = { patientNew, patientPatch, }; // controllers.js const validate = require('./validators'); const { ValidationError } = require('../common/errors'); function create (ctx) { const patientData = validate.patientNew(ctx.request.body); //       Error,             Error if (patientData instanceof Error) return ValidationError(ctx, patientData); // ...create new patient } function update (ctx) { const patientData = validate.patientPatch(ctx.request.body); if (patientData instanceof Error) return ValidationError(ctx, patientData); // ...update patient data } 

common / errors.js
const trava = require ('trava');

função ValidationError (ctx, params) {
if (params instanceof Error) {
params = trava.ValidationError.extractData (params);
}
ctx.body = {
código: 'VALIDATION_ERROR',
params,
};
ctx.status = HttpStatus.BAD_REQUEST;
}

Embora o exemplo seja muito simples, ele não pode ser chamado de simplificado. Em uma aplicação real, apenas validadores serão complicados. Você também pode validar no middleware - o validador é aplicado inteiramente ao contexto ou ao corpo da solicitação.

No processo de trabalho e uso da validação, chegamos à conclusão de que validadores síncronos simples e mensagens de erro simples são suficientes. De fato, chegamos à conclusão de que usamos apenas duas mensagens: "NECESSÁRIO" e "INVÁLIDO", localizadas no front-end, juntamente com as solicitações dos campos. Outras verificações que exigem ações adicionais (por exemplo, no registro para verificar se esse correio já existe) estão fora do escopo da validação. De qualquer forma, a grama não é sobre esse caso.

Em conclusão


Neste breve artigo, descrevi quase toda a funcionalidade da biblioteca. Fora do escopo do artigo, existem vários auxiliares que simplificam a vida. Peço detalhes no github github.com/uNmAnNeR/travajs .

Precisávamos de uma ferramenta que pudesse ser personalizada o máximo possível, na qual não houvesse nada supérfluo, mas, ao mesmo tempo, houvesse tudo o necessário para o trabalho diário. E acho que em geral isso foi alcançado, espero que alguém também facilite a vida. Terei o maior prazer em desejos e sugestões.
Para a saúde.

Source: https://habr.com/ru/post/pt462189/


All Articles