0
votes

Struggling to figure out how I can do this since it's a different type of "array validation".

Essentially, I want to take dynamic sets of rest parameters from function arguments and validate them against a schema for a REST api. For example:

@Validation(Schema.update)
public async update(id: string, model: Model) => { }

Then use my decorator to intercept the function call and perform validation based on the schema provided:


export function Validation(schema: ObjectSchema) {
  return (target: any, propName: string | symbol, descriptor: PropertyDescriptor): void => {
    const originalMethod = descriptor.value!;

    descriptor.value = function (...rest: any[]): ApiResult {
      const { error } = schema.validate({ ...rest });

      if (error) {
        console.error(error.message);
        return new ErrorApiResult({ statusCode: 400, message: error?.message });
      }

      return originalMethod.apply(this, arguments);
    };
  };
}

Currently, I want to note that this works perfectly except I have to create my validation schema like so:

export const Schema = {
  update: Joi.object({
    0: Joi.string(),
    1: ModelSchema,
  }),
}

The main problem with this is that the validation error messages Joi generates label the fields like 0.user.name instead of model.user.name, etc.

I would like to instead write the above endpoint schema like:

export const Schema = {
  update: Joi.object({
    id: Joi.string().required(),
    model: ModelSchema,
  }),
}

But I'm at a loss as to how. Looking at Joi.array(), it seems to be designed only to handle collections of objects, and not strict argument arrays.

EDIT: I've tried to use the .label() method to change the label in error messages, but this doesn't work if the error lies with a nested key. For example, it works if my id argument doesn't validate properly, but it doesn't display properly if my ModelSchema fails to validate on a child property. It still displays as "1.user.name" in error messages.

1
This just bypasses arrays entirely, but have you considered passing all your function arguments in an object? In the age of destructuring, a signature like public async update({ id: string, model: Model }) isn't unheard of. That would provide a structure that already matched the way you want to write your schema. - kdau
Actually, I did consider that. But because of the type safety of Typescript it seems like a hassle to have to define separate interfaces for all of my API methods if I want to avoid using any typing. - Trevor Elliott

1 Answers

0
votes

I was not able to solve my issue with defining Joi validation on a specific array, but I was able to use my Joi schema metadata to build a custom object that satisfies the Joi schema, as follows:

export function Validation(schema: ObjectSchema) {
  return (target: any, propName: string | symbol, descriptor: PropertyDescriptor): void => {
    const originalMethod = descriptor.value!;

    descriptor.value = function (...rest: any[]): ApiResult {
      const validationObject: any = {};
      const keys: string[] = Array.from((schema as any)._ids._byKey.keys());
      for (let i = 0; i < keys.length; i++) {
        validationObject[keys[i]] = rest[i];
      }
      const { error } = schema.validate(validationObject);

      if (error) {
        console.error(error.message);
        return new ErrorApiResult({ statusCode: 400, message: error?.message });
      }

      return originalMethod.apply(this, arguments);
    };
  };
}

I have to break Typescript's type safety to access these fields at runtime. So this isn't super ideal. But as long as the order of the keys in the validation object matches the order of the arguments in my function I'm validating, it does work.