Search code examples
javascriptjoi

How to access global object property from relative property key using Joi expression


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' } }


Solution

  • 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>