cesarParra / expression

Evaluate formulas through Apex code.
https://cesarparra.github.io/expression/
MIT License
18 stars 1 forks source link

Batch support for Custom Function? #129

Closed liumiaowilson closed 7 months ago

liumiaowilson commented 7 months ago

Hi, when we run the same expression against a batch of records in apex, we may use some custom function to make apex callouts to fetch some data. I noticed that the custom function signature is like: Object run(List<Object>), which does not support bulk function invocation. So when we run the expression during the loop of records, we may make one callout for each method invocation. Do we have any more efficient way to handle this? Thanks in advance.

cesarParra commented 7 months ago

Hi @liumiaowilson.

You should be able to receive and return collections (lists/maps) as Object as well, you would only need to do the correct casting.

So for example I could have a custom function like this which just appends a string to each Id (in your case you would call your endpoint in bulk):

global with sharing class RestFetcher implements IExpressionFunction {
    global Object execute(List<Object> args) {
        List<SObject> receivedRecords = (List<SObject>) args[0];
        List<String> accountIds = new List<String>();
        for (SObject acc : receivedRecords) {
            accountIds.add(acc.Id + ' ' + 'Custom Modification To Record');
        }
        return accountIds;
    }

where I receive an argument which is a list in the index 0 of the parameters, and then I can return back a list as well.

In the expression I can then call then use the returned list to continue modifying the data as desired

image
liumiaowilson commented 7 months ago

Yeah this approach works. But can I have admin configure expressions to run like this: RestFetcher(id) = true as a dynamic criteria to filter a batch of records in memory in apex?

cesarParra commented 7 months ago

I'd need to be more familiar with the implementation/use case.

You have the option of either passing more info to the Apex side through arguments, or on the other hand doing the filtering on the Expression side with the data that is coming back.

But whichever path you will probably need to have a separation between the function call to the Rest API and the filtering you want to do, since it won't really be able to batch it up for you since it won't have the context of what is coming back from the rest call. It'd be similar to apex where if you want to do something in bulk first you need to create the list, and then perform the operation in different steps.

liumiaowilson commented 7 months ago

The scenario is like this: we have a custom rule that admins can specify an expression as a criteria to filter the records that we want to execute some certain actions on a daily basis. I would like to use the flexibility of the expression so that admins can specify the criteria for each record, instead of handling a collection of records(too complex for admins to configure). We can provide some custom functions that wrap our existing call-outs and process the output in the functions so that admins can easily use them. It would be ideal if the custom functions can execute like invocable actions, which support passing in bulkified parameters and returning bulkified results. Hope that I have made myself clear.

cesarParra commented 7 months ago

Got it, that makes sense. It might be possible but you will probably need to do some trickery in the custom function to hide some of the complexity away.

For example you could do the bulk call once and hold it in-memory in a static variable or in a Singleton to then grab the information as needed from memory, and thus just calling the API once (the first time the function is called). I have an example of doing that for a function that simulates getting product prices: https://github.com/cesarParra/expression/blob/main/unpackaged/examples/expression-conf/main/default/custom-functions/GetListPrice.cls

All prices are fetched in the beginning but subsequent calls just grab from the cache so I can do GETLISTPRICE(Id) on a record-by-record basis.

Could be other ways of achieving it but that's just an idea.