Saltar al contenido principal

Agregar una regla de validación en el servidor

Objetivo​

Registrar una regla en el servidor que rechace un registro no válido antes de guardarlo; por ejemplo, que rechace una factura cuyo total sea negativo.

Una regla así es un paso de complemento: un script de BS Lang asociado a la escritura en una tabla. Es el mismo lenguaje que ejecuta una API personalizada, dirigido a un guardado en lugar de a una ruta.

Requisitos previos​

  • Una tabla a la que asociar el paso (por ejemplo, par_invoice con un campo par_total).
  • El rol Maker.
  • Una solución abierta: el paso pasa a ser uno de sus componentes.

Pasos​

  1. En el área de creadores, abra su solución y elija Pasos de complemento.

  2. Pulse Nuevo.

  3. Asigne al paso un nombre para mostrar («Rechazar total negativo»). El nombre lógico se rellena solo mientras escribe, con el prefijo del editor de su solución: el paso pertenece a la solución, sea cual sea la tabla que vigila.

  4. Elija la tabla que vigila el paso, la escritura a la que reacciona (Crear, Actualizar o Eliminar) y la fase:

    • Antes de la escritura es donde se rechaza o se cambia un registro. Si se rechaza aquí, el registro nunca se guarda.
    • Después de la escritura es donde se actúa sobre lo que se guardó. Se ejecuta cuando el registro ya está guardado, así que no puede rechazarlo: si llama a throwValidationError, el formulario pasa al registro guardado y dice El cambio se guardó, pero un paso que se ejecuta después de la escritura indicó después….
    • Antes de la validación se ejecuta lo más pronto posible, para comprobaciones generales. Está dentro de la misma transacción que Antes de la escritura, así que un rechazo ahí también significa que no se guarda nada.
  5. Deje Se ejecuta en Dentro de la escritura salvo que tenga un motivo para no hacerlo; consulte la nota más abajo.

  6. Establezca el orden si más de un paso comparte esta tabla, escritura y fase; el número más bajo se ejecuta primero.

  7. Pulse Guardar. El paso ya existe, con un script inicial.

  8. Selecciónelo en la lista y pulse Editar código. La lista se sustituye por el editor:

    if (getValue("par_total") < 0) {
    throwValidationError("Total cannot be negative.");
    }

    Pulse el ojo de la barra de herramientas para ver todas las funciones que puede llamar el script, con lo que recibe y devuelve cada una. Pulse Guardar y después Cerrar para volver a la lista.

  9. Intente crear un registro que incumpla la regla. El guardado se rechaza con la frase que pasó a throwValidationError, y no se guarda nada.

Qué ocurre entre bastidores​

Cuando se crea, actualiza o elimina un registro, la plataforma ejecuta todos los pasos habilitados registrados para esa tabla y esa escritura, en orden ascendente, en la fase correspondiente. Un paso Antes de la escritura se ejecuta dentro de la transacción, así que rechazar ahí significa que el registro nunca se guarda. En esa fase, un paso también puede dar un valor predeterminado o corregir un valor con setValue("field", value).

El registro se valida al guardarlo (el contenedor contra el esquema de paso de complemento, el script contra la gramática de BS Lang), así que un script que no se puede analizar se rechaza indicando la línea del problema, en lugar de fallar más tarde delante de un usuario.

Por qué esto y no una regla de negocio​

Una regla de negocio se ejecuta en el navegador, en un formulario. Es la herramienta adecuada para lo que ve una persona: mostrar un campo, hacerlo obligatorio, darle un valor predeterminado mientras escribe.

Un paso de complemento se ejecuta en el servidor, dentro de la transacción. Se aplica sea cual sea la manera en que se guardó el registro: desde un formulario, desde la API, desde la importación de una solución, desde una aplicación personalizada. Si una restricción tiene que cumplirse de verdad, va aquí. Las dos son complementarias: la regla hace que el formulario sea agradable, el paso hace que los datos sean ciertos.

Leer y escribir otros registros​

Un paso puede llegar a registros distintos del que se está guardando:

// Read a record. Its fields are the properties of what comes back, plus id.
var account = getRecord("par_account", getValue("par_account_id"));
if (account.par_status == "on hold") {
throwValidationError("That account is on hold.");
}

// Read several, and walk them.
var open = queryRecords("par_invoice", "par_status", "open");
for (var i: int = 0; i < open.length; i++) {
// open[i].par_total, open[i].id, ...
}

// Write: the fields you want, in one go. You get the new record's id back.
var noteId = createRecord("par_audit_note", { par_text: "Approved automatically." });
updateRecord("par_account", account.id, { par_reviewed: true });

Conviene saber tres cosas:

  • Se ejecuta en nombre de quien guardó el registro, no como administrador. Si esa persona no puede leer una tabla, su paso tampoco. Un script nunca es una manera de saltarse un permiso.
  • Una escritura es una sola escritura. Pase todos los campos en un único objeto. Se aplican las reglas habituales, así que también se ejecuta un paso registrado en esa tabla.
  • No escriba en la tabla que vigila su paso salvo que lo proteja: la escritura vuelve a activar su propio paso. La plataforma detiene el bucle a las ocho veces, pero la solución es una condición, no el límite. depth() le indica a qué profundidad está.

Un registro que no existe se devuelve como nada, así que if (isEmpty(getRecord(...))) es la manera de comprobarlo antes de leer un campo. La lectura de varios registros está limitada a 200 filas: una pregunta sobre todas las filas es un informe, y el diseñador de consultas es el lugar para ella. countRecords no está limitado: pide a la base de datos un recuento en lugar de obtener filas, así que es correcto haya las que haya.

Hacer una pregunta más difícil​

queryRecords compara un campo con un valor. Cuando eso no basta (un intervalo, varias condiciones, una ordenación, una columna de una tabla combinada), use runQuery, que recibe el documento de consulta propio de la plataforma: lo mismo que genera el generador de Búsqueda avanzada y que guarda una consulta guardada.

var cheap = runQuery({
entity: "par_product",
filter: {
connector: "And",
conditions: [
{ field: "par_price", operator: "LessThan", value: 10 },
{ field: "par_discontinued", operator: "Equals", value: false }
]
},
order: [{ field: "par_price", descending: true }],
top: 20
});

Los operadores son Equals, NotEquals, Contains, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual, IsNull, IsNotNull e In; In recibe una lista, así que «el estado es Nuevo o Activo» es una sola condición en lugar de dos que combine a mano.

También cuenta y agrupa. Agregue un aggregate y obtendrá grupos en lugar de filas, cada uno como un objeto plano con sus claves y sus medidas:

var byOwner = runQuery({
entity: "par_order",
aggregate: {
measures: [{ alias: "total", function: "Sum", field: "par_amount" }],
groupBy: [{ field: "par_owner" }]
}
});
// byOwner[0].par_owner, byOwner[0].total

Es una pregunta que ninguna cantidad de llamadas a queryRecords podría responder, porque sumar en un script obliga a obtener antes todas las filas, que es justo lo que prohíbe el límite de 200 filas.

Preguntar por dos tablas a la vez​

Una factura conoce el id de su cliente y nada más sobre él. «Facturas sin pagar de un cliente cuyo límite de crédito es inferior a 5000» es, por tanto, una pregunta sobre dos tablas, y links combina la segunda bajo un alias:

var rows = runQuery({
entity: "par_invoice",
select: ["par_name", "par_amount"],
links: [
{
alias: "cust",
entity: "par_customer",
'from': "par_customerid",
to: "id",
kind: "Inner",
select: ["par_name", "par_credit_limit"]
}
],
filter: { conditions: [
{ field: "cust.par_credit_limit", operator: "LessThan", value: 5000 }
] }
});

report(rows[0].cust.par_name);

Hay tres cosas que saber, y la primera le va a pillar:

Ponga 'from' entre comillas. from es una palabra que BS Lang ya usa, así que un from: sin comillas es un error de análisis: su script no se ejecuta, y el mensaje habla del lenguaje, no de la consulta. to, select, operator y los demás son nombres normales y no necesitan comillas. Dentro de page, number es la única otra clave que las necesita.

from y to indican el sentido del recorrido. from es el campo de la tabla desde la que viene; to es el campo de la tabla a la que va. Si una combinación no devuelve nada cuando está seguro de que debería devolver algo, es lo primero que debe comprobar.

Una columna combinada se lee bajo su alias: rows[0].cust.par_name, no rows[0].par_name. Para eso sirve el alias: dos tablas pueden tener un par_name, y dentro de una misma fila deben mantenerse separados. Un filtro nombra una columna combinada de la misma manera, como cust.par_credit_limit.

Use kind: "Outer" para conservar las filas que no tienen coincidencia: una oportunidad sin cuenta sigue siendo una oportunidad. Sus campos de alias se devuelven vacíos, así que isEmpty(rows[0].cust.par_name) es la manera de comprobarlo. Y cada tabla combinada se comprueba por sí misma contra los permisos de quien llama: una combinación no es una manera de leer una tabla que esa persona no podría abrir.

Reutilizar una consulta que alguien ya construyó​

Para una consulta realmente complicada, no escriba el documento en su script. Constrúyala en Búsqueda avanzada, guárdela y llámela por su nombre:

var overdue = runSavedQuery("par_overdue_invoices");

Suele ser la mejor respuesta, por tres motivos que no tienen nada que ver con escribir menos. La consulta se construye y se prueba en un diseñador, a cargo de quien entiende la pregunta. Es un componente de solución, así que tiene versión y viaja a Prueba y a Producción con todo lo demás. Y cuando la pregunta cambia, cambia en un solo sitio: todos los pasos y las API personalizadas que la nombran cambian a la vez.

Las dos se ejecutan con los permisos de quien llama, exactamente igual que queryRecords, y cada tabla combinada se comprueba por sí misma. Una consulta guardada no es una manera de leer algo que usted mismo no podría leer.

Compartir lógica entre pasos​

Un paso puede importar un script de la solución, igual que una regla de negocio:

import 'par_order_rules';

if (needsApproval(getValue("par_total"))) {
throwValidationError("This needs approval.");
}

Escriba la regla una vez, en Scripts, e impórtela desde todos los pasos, y desde una API personalizada que tenga que responder a la misma pregunta. Eso es lo que impide que dos copias de un mismo umbral discrepen el día en que alguien lo cambie. El ejemplo business-rules-shared-server-logic está construido justo alrededor de esto.

Pasos asíncronos​

Se ejecuta → Después de la respuesta entrega el paso a una cola en lugar de ejecutarlo en línea. El registro se guarda, quien llama recibe su respuesta y el paso se ejecuta poco después.

De ello se derivan varias cosas, que no son tanto restricciones como lo que significa «después»:

  • Tiene que ser Después de la escritura. La pantalla cambia la fase por usted cuando lo elige. Un paso que se ejecuta cuando la respuesta ya se ha enviado no puede cambiar ni rechazar la escritura, así que las fases anteriores serían un momento que nunca puede llegar.
  • No puede rechazar nada. throwValidationError en un paso asíncrono registra un error asociado al paso; el registro sigue guardado. Todo lo que tenga que bloquear un guardado es síncrono.
  • Puede ejecutarse dos veces. La entrega es de al menos una vez, así que escriba el paso de forma que ejecutarlo dos veces no cause daño; es la misma regla que siguen los complementos asíncronos de Dataverse.
  • Se ejecuta en nombre de la persona que guardó, con los roles de seguridad que tenía al guardar. Puede leer y escribir registros y enviar correo electrónico igual que un paso que se ejecuta durante el guardado. No puede escribir archivos en el almacenamiento. En un guardado hecho por un visitante del portal no puede hacer nada de esto.
  • recordId() indica de qué registro se trata. Recibe el guardado, no el registro tal como está ahora, así que lea el registro con getRecord(entityName(), recordId()) cuando necesite sus valores actuales, y vuelva a escribir con updateRecord. Compruebe el registro antes de actuar: si su paso escribe en el registro sobre el que se ejecuta, esa escritura vuelve a ejecutar el paso.

Úselo para trabajo por el que quien llama no debe esperar: enviar una notificación, llamar a otro sistema, actualizar un acumulado.

Solución de problemas​

  • «El nombre lógico debe llevar el prefijo …». El nombre debe empezar por el prefijo del editor de su solución y un guion bajo (por ejemplo, par_). La tabla que elija no lo cambia: un paso sobre la tabla estándar Cuenta se sigue llamando par_….
  • «El script de BS Lang no es válido». El script no se pudo analizar; el mensaje indica la línea.
  • El paso no se ejecutó. Compruebe que esté En ejecución y no Detenido, y que apunte a la tabla, la escritura y la fase correctas. Si se ejecuta Después de la respuesta, dele un momento: está en cola, no en línea.
  • Nada bloquea una actualización. Un paso vigila una sola escritura. Registre un segundo paso para Actualizar si también hay que proteger las actualizaciones.
  • import no se resuelve. Compruebe que el nombre del módulo coincida exactamente con el nombre lógico del script de la solución, y que el script exporte lo que está llamando (export myFunction;). Un script sin instrucción export no enlaza nada.

Escenarios relacionados​