TomFrost / Jexl

Javascript Expression Language: Powerful context-based expression parser and evaluator
MIT License
561 stars 92 forks source link

Add lodash integration to Readme? #73

Closed cunneen closed 4 years ago

cunneen commented 4 years ago

Hi, this is a fantastic little library. Thanks for contributing it!

I've been playing with it and seeing if it'll work for us as a safe way of letting users define their own calculations which we can store in the database and apply to their own docs at runtime. Your Transforms feature is fantastic.

Using it, I thought I'd try binding a custom transform to lodash. It turns out that this can be pretty handy, and goes some way towards addressing Issue #25 (at least the example use case) and possibly #43. So I thought I'd share it here, and ask 2 questions:

Lodash integration

The Transforms feature allows us to bind other 3rd-party libraries, for example Lodash :

const _ = require('lodash');
const jexl = require('jexl');

jexl.addTransform('_', (inputObj, lodashFuncName, ...arguments) => { 
  if(_.hasOwnProperty(lodashFuncName)){ 
    return _[lodashFuncName].apply(this, [inputObj].concat(arguments));
  } 
});

Then you can use many of the lodash functions, for example:

const doc2 = { employees: 
   [ { first: 'Sterling', last: 'Archer', age: 36 },
     { first: 'Malory', last: 'Archer', age: 75 },
     { first: 'Lana', last: 'Kane', age: 33 },
     { first: 'Cyril', last: 'Figgis', age: 45 },
     { first: 'Cheryl', last: 'Tunt', age: 28 } ],
  retireAge: 62 }

jexl.evalSync('employees|_("meanBy","age")', doc2)
// output: 43.4

jexl.evalSync('employees|_("maxBy","age")', doc2)
// output: { first: 'Malory', last: 'Archer', age: 75 }
jexl.evalSync('employees|_("minBy","age")', doc2)
// output: { first: 'Cheryl', last: 'Tunt', age: 28 }
jexl.evalSync('employees|_("map","age")', doc2)
// output: [ 36, 75, 33, 45, 28 ]

// === you can combine them! === 
jexl.evalSync('employees|_("map","age")|_("sum")', doc2)
// output: 217
cunneen commented 4 years ago
// === more examples

// find the youngest three
jexl.evalSync('employees|_("sortBy","age")| _("slice",0,3)', doc2);
/* [ { first: 'Cheryl', last: 'Tunt', age: 28 },
  { first: 'Lana', last: 'Kane', age: 33 },
  { first: 'Sterling', last: 'Archer', age: 36 } ]
*/

// key the results by last name
jexl.evalSync('employees|_("sortBy","age")|_("keyBy","last")', doc2)
/* { Tunt: { first: 'Cheryl', last: 'Tunt', age: 28 },
  Kane: { first: 'Lana', last: 'Kane', age: 33 },
  Archer: { first: 'Malory', last: 'Archer', age: 75 },
  Figgis: { first: 'Cyril', last: 'Figgis', age: 45 } }
*/
TomFrost commented 4 years ago

Gosh, I love this from the functionality standpoint, but it scares me in terms of security. I (and many other Jexl users) use this library exactly how you are -- storing user-specified expressions in a DB and running them verbatim. One of the design goals of Jexl is to make an unquestionably safe execution environment for all expressions, with the only caveat being that Jexl is only as safe as your custom transforms and operators.

Because this allows the execution of not only your code but also any code in lodash now and in the future, you can't say with certainty what all a user will be capable of executing now or in the future. If lodash ever has a security issue, so do you. If there's a particularly heavy function call, users can call it repeatedly.

So, if this pattern is a win for you and you're watching its usage, go for it :). I don't think this use case should be encouraged in the Readme though -- but I do appreciate you sharing it!

cunneen commented 4 years ago

No problem. In my case, it's only (non-technical) administrators of the system who can define the expressions, they are only executed client-side, and lodash is (of course) locked to a specific version. But I do understand the concern.