joi icon indicating copy to clipboard operation
joi copied to clipboard

How to use the value of a sibling field as the label of another field?

Open nicholas-ochoa opened this issue 2 years ago • 1 comments

Support plan

  • is this issue currently blocking your project? (yes/no): no
  • is this issue affecting a production system? (yes/no): no

Context

  • node version: 14.17.3
  • module version: 17.4.2
  • environment (e.g. node, browser, native): node
  • used with (e.g. hapi application, another framework, standalone, ...): express, typescript
  • any other relevant information:

How can we help?

I have a process on my API that accepts arbitrary inputs from an Angular application to set values for a report which is generated. I'm trying to validate the input based on other parameters being passed in (such as the field data type) which works great, however when a value fails the validation the error messages are too vague / technical such as "params[2].field_value" must be a valid date.

Here is an example input that I'm passing in, along with the validation code I'm using: (note the invalid date on the 3rd entry in the array on "End Date", in the "field_value" property)

{
  "params": [
   {
      "field_seq": 1,
      "field_label": "Cust ID",
      "field_type": "varchar",
      "field_required": "required",
      "field_value": "1CALJAM"
    },
    {
      "field_seq": 2,
      "field_label": "Start Date",
      "field_type": "date",
      "field_required": "required",
      "field_value": "08/01/2021"
    },
    {
      "field_seq": 3,
      "field_label": "End Date",
      "field_type": "date",
      "field_required": "required",
      "field_value": "08/3120212"
    },
    {
      "field_seq": 4,
      "field_label": "BOL",
      "field_type": "varchar",
      "field_required": "required",
      "field_value": "Y"
    }
  ]
}
function validate(req: Request) {
  const paramSchema = Joi.object({
    field_seq: Joi.number().positive().precision(0).max(100).required(),
    field_label: Joi.string()
      .trim()
      .max(255)
      .required(),
    field_type: Joi.string().trim().max(12).required(),
    field_required: Joi.string().trim().max(12).required(),
    field_value: Joi.when('field_required', { is: Joi.string().regex(/^required$/), then: Joi.required() })
      .when('field_type', { is: Joi.string().regex(/^varchar$/), then: Joi.string().trim().max(4000) })
      .when('field_type', { is: Joi.string().regex(/^number$/), then: Joi.number() })
      .when('field_type', {
        is: Joi.string().regex(/^date$/),
        then: Joi.date(),
      }),
  });

  const schema = Joi.object({
    sessionId: Joi.string().trim().max(255).required(),
    reportName: Joi.string().trim().max(255).required(),
    params: Joi.array().required().min(0).items(paramSchema),
  });

  return schema.validate(req.body, { debug: true }).error;
}

I've tried using the label() setting and this does allow me to change the label for field_value to something else, but so far only a static string has worked successfully.

I made an attempt to use function calls on custom() to set a field value and later get it, but the order that the functions are called results in this not working. Here is an example of that validation code:

function validate(req: Request) {
  let fieldLabel: string;

  function setFieldLabel(label: string) {
    console.log('setFieldLabel:', label);
    fieldLabel = label;
  }

  function getFieldLabel(): string {
    console.log('getFieldLabel:', fieldLabel);
    return fieldLabel ?? 'empty';
  }

  const paramSchema = Joi.object({
    field_seq: Joi.number().positive().precision(0).max(100).required(),
    field_label: Joi.string()
      .trim()
      .max(255)
      .required()
      .custom((value) => {
        setFieldLabel(value);
      }),
    field_type: Joi.string().trim().max(12).required(),
    field_required: Joi.string().trim().max(12).required(),
    field_value: Joi.when('field_required', { is: Joi.string().regex(/^required$/), then: Joi.required() })
      .when('field_type', { is: Joi.string().regex(/^varchar$/), then: Joi.string().label(getFieldLabel()).trim().max(4000) })
      .when('field_type', { is: Joi.string().regex(/^number$/), then: Joi.number().label(getFieldLabel()) })
      .when('field_type', {
        is: Joi.string().regex(/^date$/),
        then: Joi.date().label(getFieldLabel()),
      }),
  });

  const schema = Joi.object({
    sessionId: Joi.string().trim().max(255).required(),
    reportName: Joi.string().trim().max(255).required(),
    params: Joi.array().required().min(0).items(paramSchema),
  });

  return schema.validate(req.body, { debug: true }).error;
}

Ideally, I would like to just replace "params[2].field_value" must be a valid date with (in this specific example) "End Date" must be a valid date, with the label value being pulled from the value of field_label in the same object.

Is something like this even possible?

nicholas-ochoa avatar Aug 11 '21 19:08 nicholas-ochoa

I ended up getting this working by modifying the output error message. This feels somewhat hacky to me, but will work well enough unless there is a better solution.

function validate(req: Request) {
  const paramSchema = Joi.object({
    field_seq: Joi.number().positive().precision(0).max(100).required(),
    field_label: Joi.string().trim().max(255).required(),
    field_type: Joi.string().trim().max(12).required(),
    field_required: Joi.string().trim().max(12).required(),
    field_value: Joi.when('field_required', { is: Joi.string().regex(/^required$/), then: Joi.required() })
      .when('field_type', { is: Joi.string().regex(/^varchar$/), then: Joi.string().trim().max(4000) })
      .when('field_type', { is: Joi.string().regex(/^number$/), then: Joi.number() })
      .when('field_type', {
        is: Joi.string().regex(/^date$/),
        then: Joi.date(),
      }),
  });

  const schema = Joi.object({
    sessionId: Joi.string().trim().max(255).required(),
    reportName: Joi.string().trim().max(255).required(),
    params: Joi.array().required().min(0).items(paramSchema),
  });

  const error = schema.validate(req.body, { debug: true }).error;

  if (error?.details.length > 0 && error?.details[0].path.includes('params')) {
    error.details[0].message = error.details[0].message.replace(
      error.details[0].context.label,
      error._original.params[error.details[0].path[1]].field_label
    );
  }

  return error;
}

nicholas-ochoa avatar Aug 11 '21 20:08 nicholas-ochoa