Skip to content
This repository has been archived by the owner on Apr 12, 2022. It is now read-only.

Calculated properties plugin #2146

Merged
merged 43 commits into from
Aug 25, 2021
Merged

Calculated properties plugin #2146

merged 43 commits into from
Aug 25, 2021

Conversation

teallarson
Copy link
Contributor

@teallarson teallarson commented Aug 16, 2021

DONE:

  • generates an app
  • generates a source
  • generates a property
  • replaces mustached variables from a property's customFunction option, checks for illegal strings (require, env)
  • Sorts config validations, application, and processing so that calculated properties wait on the properties they reference
  • Executes the function from the customFunction string in a Node VM instance per profile
  • saves value returned from customFunction as a new ProfileProperty
  • recalculates on each run (like all other profileProperties)
  • Testing coverage:
    • cli
    • validate
    • property-imports for each type (based on query sources)
    • property-imports also tests that the function will throw with invalid input

TO DO AFTER COMPLETE (www):

  • (in progress) Docs
    • basics about the tool in code config docs
    • important to note:
      • profile property values are parsed into the function as strings
      • we actually use a property's name in the mustache vars, not its id
  • What's New
  • Add a calculated property to roo demo -c

@teallarson teallarson added the enhancement New feature or request label Aug 16, 2021
Copy link
Member

@evantahler evantahler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work so far! Adding some comments for later today

@teallarson teallarson force-pushed the calculated-properties-plugin branch 2 times, most recently from c2f014d to 3cf2682 Compare August 23, 2021 20:59
@teallarson
Copy link
Contributor Author

  • topological sort with mustache vars works across plugins now
  • completed unit testing
  • vm executes the function now instead of just compiling and exporting it

core/src/initializers/codeConfig.ts Outdated Show resolved Hide resolved
core/src/classes/codeConfig.ts Show resolved Hide resolved
core/src/models/Property.ts Outdated Show resolved Hide resolved
core/src/modules/plugin.ts Outdated Show resolved Hide resolved
core/src/modules/plugin.ts Outdated Show resolved Hide resolved
core/src/utils/pluginDetails.js Show resolved Hide resolved
Comment on lines +38 to +42
calculatedPropertyValue = vm.run(
`const toRun = ${populatedFunction}; module.exports = toRun();`
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 the method is being defined and run in the VM, not our 'real' code

prerequisiteIds.push(
...mustachePrerequisiteIds.map((p) => `property:${p}`)
);
// - property with mustache dependency
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now will sort ANY property with a mustache dependency, not just query source properties

): Promise<string> {
if (string.indexOf("{{") < 0) return string;

const data = await getProfileData(profile);
return MustacheUtils.strictlyRender(string, data);
if (strict === true) return MustacheUtils.strictlyRender(string, data);
return MustacheUtils.render(string, data);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows non-strict renders for things like returning mustached properties for calculated properties. That means someone can now do things like calculate if ("{{last_purchase_category}}" !== ""){...}

];
}

function customFunction () {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is written outside of the config object to allow for helpful, human-friendly formatting and is then parsed to a string in the template for the plugin to read.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

We'll probably add a link to the docs here once they are ready too

sandbox: {},
argv: [],
env: {},
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're allowing console usage, but keeping everything else locked down. External libraries cannot be imported and neither can env variables.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

test("it evaluates date strings as expected", async () => {
const fn = `() => {
const date = new Date("{{lastLoginAt.iso}}");
return date.toISOString();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

important to note in docs that we return date properties as an object with multiple date formats!

@teallarson teallarson marked this pull request as ready for review August 25, 2021 16:20
@evantahler evantahler linked an issue Aug 25, 2021 that may be closed by this pull request
@@ -55,6 +55,7 @@ function getPluginManifest() {
grouparoo: { plugins: [] },
},
plugins: [],
missingPlugins: [],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for logging purposes

profile,
false
);
//fail at every level if someone tries to require a library... this should never be allowed to hit vm.run
Copy link
Contributor Author

@teallarson teallarson Aug 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that running roo validate or roo apply will fail. It fails a bit more abruptly than with other config errors, but I think that's warranted. We don't want anything questionable attempting to be executed. This is one area not covered in our test suite because I couldn't get it to test without throwing the error straight to the console. Open to suggestions there or leaving as is!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be tested! Wound't this throw in the way you are already testing in __tests__/import/import-property.ts ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm - this check is to "fail fast" with grouparoo validate if you try to require something or read process.env. Otherwise, the function would be valid, but produce results you don't expect as we block / null out those at runtime.

We can test that process.env is empty with a test like

const fn = `() => { return process.env.DATABASE_URL }`; 

and show that it's undefined which might be a good idea.

Copy link
Contributor Author

@teallarson teallarson Aug 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't throwing the exact same way, but I've changed it to now

@teallarson
Copy link
Contributor Author

teallarson commented Aug 25, 2021

Known limitation: if I generate a property, then roo run, change the function, then roo run again, it will appear to be stuck (does stuff, but # of profiles and imports doesn't change) for a decent chunk of time (>1-2 mins) then eventually will run and recalculate. This is using our 1000 sample profiles, so could be very slow with larger data sets if someone changed their config.

@evantahler
Copy link
Member

Known limitation: if I generate a property, then roo run, change the function, then roo run again, it will appear to be stuck (does stuff, but # of profiles and imports doesn't change) for a decent chunk of time (>1-2 mins) then eventually will run and recalculate. This is using our 1000 sample profiles, so could be very slow with larger data sets if someone changed their config.

That's interesting! Can you please write this up? I wonder if it works that way for all changed properties (and not just this new plugin). A changed Property does make an internal run...

@teallarson
Copy link
Contributor Author

Known limitation: if I generate a property, then roo run, change the function, then roo run again, it will appear to be stuck (does stuff, but # of profiles and imports doesn't change) for a decent chunk of time (>1-2 mins) then eventually will run and recalculate. This is using our 1000 sample profiles, so could be very slow with larger data sets if someone changed their config.

That's interesting! Can you please write this up? I wonder if it works that way for all changed properties (and not just this new plugin). A changed Property does make an internal run...

Hmm. Yeah, it's not a calculated properties only thing! Adding to tracker.

Copy link
Member

@evantahler evantahler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great! I think there are few things more to test and a little cleanup... and then this is ready to be merged 🎉!

];
}

function customFunction () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

We'll probably add a link to the docs here once they are ready too

sandbox: {},
argv: [],
env: {},
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

profile,
false
);
//fail at every level if someone tries to require a library... this should never be allowed to hit vm.run
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be tested! Wound't this throw in the way you are already testing in __tests__/import/import-property.ts ?

profile,
false
);
//fail at every level if someone tries to require a library... this should never be allowed to hit vm.run
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm - this check is to "fail fast" with grouparoo validate if you try to require something or read process.env. Otherwise, the function would be valid, but produce results you don't expect as we block / null out those at runtime.

We can test that process.env is empty with a test like

const fn = `() => { return process.env.DATABASE_URL }`; 

and show that it's undefined which might be a good idea.

@@ -20,7 +20,7 @@ async function calculateProfilePropertyValue(
);

//fail at every level if someone tries to require a library... this should never be allowed to hit vm.run
const illegalStrings = [`require(`, `process.env`];
const illegalStrings = [`require(`, `process.env`, `async`];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, that's one way to do it!

We will need to get async functions working in the next version of this (when we want to support fetch, etc) but I guess we don't need it now

@teallarson teallarson merged commit 5d13788 into main Aug 25, 2021
@teallarson teallarson deleted the calculated-properties-plugin branch August 25, 2021 22:12
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature Request: Combined Profile Properties
3 participants