1

I'm trying to set a default value of a property based on a global context passed through at validate and an existing property of the object being validated. I just can't seem to use the value of the property of the object as a key for the global context.

const context = {
    letters: {
        a: 1,
        b: 2,
        c: 3,
    },
};

const s = Joi.object().keys({
    letter: Joi.string().valid(...['a', 'b', 'c']),
    num: Joi.number().default(Joi.expression('{$letters}.{letter}'))
})

const test = {
    letter: 'a',
}

console.log(s.validate(test, { context }));

Simple example above. I have an object called letters that I place into the context. The schema will look for letter, then try to set a default value for num using the global letters as the object to pull from and the letter passed in as the key. The closest I can get is { value: { letter: 'a', num: '[object Object].a' } }

Sanket Shah
  • 2,888
  • 1
  • 11
  • 22
Individual11
  • 374
  • 4
  • 16
  • Cant u use template strings from javascript? `\`${context.letters.a}\``? – testing_22 Aug 26 '21 at 21:29
  • @testing_22 Unfortunately no, not really. If `context` is created in the scope where the Joi schema is created, it _kind of_ work, but then `context` could not be generated elsewhere and passed in during validation, which is not how Joi should work. Good idea though, and again, in certain circumstances, it would work, but not all. – Individual11 Aug 28 '21 at 11:26

2 Answers2

1

Some of the below examples that would work

Joi.expression(`{$letters[.a]}`)
Joi.expression(`{{$letters.b}}`)
Joi.expression(`{{$letters[.c]}}`)

But the problem here is we can't use variable like letter in place of a or b or c.

But we can achieve the what is required with custom function

// https://stackoverflow.com/a/6491621/14475852
Object.byString = function(o, s) {
  s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
  s = s.replace(/^\./, ''); // strip a leading dot
  var a = s.split('.');
  for (var i = 0, n = a.length; i < n; ++i) {
    var k = a[i];
    if (k in o) {
      o = o[k];
    } else {
      return;
    }
  }
  return o;
};

function customEvaluator(expression) {
  return function(parent, helpers) {
    let global = expression[0] == '$';
    let exp = '';
    for (var i = global ? 1 : 0, n = expression.length; i < n; ++i) {
      if (expression[i] == '#') {
        let part = expression.slice(i + 1);
        let end = -1;
        [' ', '.', '[', ']', '{', '}'].forEach(i => {
          let tmp = part.indexOf(i);
          if (end == -1 && tmp != -1) {
            end = tmp;
          }
        });

        end = end == -1 ? part.length : end;
        exp += parent[part.slice(0, end)];
        i += end;
      } else {
        exp += expression[i];
      }
    }

    return Object.byString(global ? helpers.prefs.context : parent, exp);
  };
}

const Joi = joi; // exported as joi in the browser bundle

const s = Joi.object().keys({
    letter: Joi.string().valid(...["a", "b", "c"]),
    num: Joi
        .number()
        .default(customEvaluator('$letters.#letter')),
});

const context = {
    letters: {
        a: 1,
        b: 2,
        c: 3,
    },
};

console.log(s.validate({ letter: "a" }, { context }));
console.log(s.validate({ letter: "b" }, { context }));
console.log(s.validate({ letter: "a", num: 4 }, { context }));
<script src="https://cdn.jsdelivr.net/npm/joi@17.4.2/dist/joi-browser.min.js"></script>
Chandan
  • 11,465
  • 1
  • 6
  • 25
0

The default method can accept

  • a function which returns the default value using the signature function(parent, helpers) where:
    • parent - a clone of the object containing the value being validated. Note that since specifying a parent argument performs cloning, do not declare format arguments if you are not using them.
    • helpers - same as those described in any.custom().

The helpers include a property prefs containing the current preferences, including a context property with the current context.

You can use this to supply a custom default value based on the context and the other properties:

const Joi = joi; // exported as joi in the browser bundle

const context = {
    letters: {
        a: 1,
        b: 2,
        c: 3,
    },
};

const s = Joi.object().keys({
    letter: Joi.string().valid(...["a", "b", "c"]),
    num: Joi
        .number()
        .default(({ letter }, { prefs: { context } }) => context.letters[letter]),
});

const test = {
    letter: "a",
};

console.log(s.validate(test, { context }));
console.log(s.validate({ letter: "b" }, { context }));
console.log(s.validate({ letter: "a", num: 4 }, { context }));
<script src="https://cdn.jsdelivr.net/npm/joi@17.4.2/dist/joi-browser.min.js"></script>
Lauren Yim
  • 12,700
  • 2
  • 32
  • 59
  • The code runs for sure, and it's definitely something I hadn't considered before, so thank you. It doesn't fully answer how to use `expression` though. The reason I want to use `expression` is so I can store the expression itself externally. That way, I can change the expression separate from the code, and it will continue function as expected. This answer requires me to change actual code in order to make updates. – Individual11 Aug 28 '21 at 12:56