Flexing Fusion Apps Filters

I enjoyed returning to the APEX Alpe Adria conference to chat with and learn from members of the APEX community. If you joined me in Maribor, thanks for your interest in extending Fusion Applications with Oracle APEX. You can download the slides from my session here. In a nutshell, using Fusion Apps business objects in APEX is now as easy as working with local tables.

Fusion Apps (FA) business object REST APIs support filtering using SQL-like predicates and sorting using object attribute names. APEX delegates all searching and sorting to the FA service by automatically using these features. In addition, APEX requests only the data for attributes the current region is using to keep payloads small and fast. These optimizations help you deliver a great user experience. This article explains how to use these features in APEX pages, List of Values, and in PL/SQL business logic.

Setting the Stage

Each Fusion Apps pillar – HCM, ERP, CX, SCM, etc. – has a set of standard business objects. It also lets admins with appropriate privilege extend the app data model. To manage data specific to a company’s unique requirements, an FA customer admin can add new custom attributes to standard objects. In some pillars, they can also create new custom business objects and relate them to other objects.

Each standard or custom object has a corresponding REST API. All share a common set of features and include custom attributes automatically. An FA business object REST service can describe itself, retrieve filtered data, and handle create, update, and delete operations. It also supports bulk operations, where a batch of changes can be done in a single transaction.

Whenever a client uses an FA REST API to create or change data, the service evaluates all appropriate business object validation rules and executes any processing logic. These include behaviors defined by the FA developers at the Oracle “factory”as well as ones added by FA admins as customizations. If the requested operation fails, the response includes one or more error messages to help the end-user understand what requires attention before submitting again. APEX automatically presents these messages to the end-user in a familiar way.

Lead Tracking for an Art Gallery

An art gallery using the Fusion CX suite might use the standard Leads object to track potential customers who have shown interest in purchasing art, together with custom objects like Artwork_c, ArtMedium_c, Artist_c, and InterestedArtworks_c to track the artworks, media, artists, and pieces a particular lead is interested in purchasing. The Resources standard object stores the staff members (a.k.a. human resources) working at the gallery who follow-up after taking ownership of a lead. The following diagram shows a simplified version of the business object model. Notice the convention that the names of custom objects and custom attributes have the _c suffix to distinguish them from the names of the standard objects and attributes.

Two Fusion Apps standard objects (Leads, Resources) and four custom objects

FA REST Filter Predicates

Fusion Apps business objects REST APIs support SQL-like filter predicates. You phrase them in terms of the case-sensitive business object attribute names. For example, to retrieve leads with a budget greater than or equal to 10000, you would use the following filter expression with the leads endpoint:

Budget_c >= 10000

You can combine conditions using conjunctions and group conditions using parentheses. So, to find leads with names that start with ‘A’ or ‘Z’ in addition to having the above budget, you can write:

Budget_c >= 10000 and (Name like 'A%' or Name like 'Z%')

For case-insensitive comparisons, use the upper() function around the attribute name on the left-hand-side of the expression. To find leads containing a letter a or z somewhere in the name, you can use:

Budget_c >= 10000 and (upper(Name) like '%A%' or upper(Name) like '%Z%')

Each filter predicate has the name of an attribute on the left hand side –optionally surrounded by upper() – followed by an operator, and finally a literal operand value on the right hand side. When using the between operator, you provide two literal values as in this example:

(Budget_c between 7000 and 1000) and (Name like '%A%' or Name like '%Z%')

When filtering on date or timestamp values, the literal values must be strings in the ISO8601 format, so to find leads created any time on December 31st, 2023 you use the following filter. It references one of the common audit attributes that include CreationDate, CreatedBy, LastUpdateDate, and LastUpdatedBy:

CreationDate between '2023-12-31T00:00:00' and '2023-12-31T23:59:59'

To filter on a date field having no time component, leave off the time portion of the literal date string. For example, to find leads whose decision date is before May 1st, 2024, use:

DecisionDate_c < '2024-05-01'

To find leads with qualified status having a rank of warm or hot, you can write a filter like:

StatusCode = 'QUALIFIED' and Rank in ('WARM','HOT')

To find leads that have specified some decision date, you can use:

DecisionDate_c is not null

Conversely, to check for leads with no decision date provided, use:

DecisionDate_c is null

When an FA REST service exposes so-called accessor attributes to related objects, you can reference them in a filter predicate. For example, to find any leads who have shown interest in artwork whose price is between 500 and 1000, you can write the filter:

InterestedArtworks_c.ArtworksObject_c.Price_c between 500 and 1000

Using an External Filter on a Page

After defining a REST Data Source of type Oracle Cloud Applications (SaaS) REST Service for a particular FA REST API, you can use it in any APEX region. Whenever a region uses a REST Data Source, the Property Editor shows the External Filter and Order By section with an Enable switch. After flipping it to the on position, you can enter an external filter clause and/or external order by clause. The syntax of both is specific to the type of the REST data source. One created for an FA REST API using the Oracle Cloud Applications (SaaS) REST Service type lets you specify any legal filter predicate we saw above.

When writing the one or more predicate expressions in your external filter, you can use a literal value for the right-hand-side operand. Alternatively, you can reference a value from APEX session state using substitution syntax like this:

(DecisionDate_c < &P3_DECISION_DATE.) and
(InterestedArtworks_c.ArtworksObject_c.Price_c 
between &P3_LOW_PRICE. and &P3_HIGH_PRICE.)
and Budget_c > 5000 

The value of the page items you reference in the external where clause can be computed by page processes or computations in the Pre-Rendering section of the page. This would come in handy if, for example, the operand for filtering on the lead’s DecisionDate_c field needed to be a function of the current date (formatted in ISO8601 format).

If the region type where you’ve configured an external filter allows end-user filtering operations as well, then any filters added by the end user are combined with the external filter using the and conjunction in the filter expression that APEX sends to the FA REST endpoint.

Using an External Order By on a Page

To configure a particular sort order for a region based on an FA REST API, use an external order by clause. The syntax is simple. Just provide a comma-separated list of case-sensitive attribute names. For example, to sort on the lead name, just mention the attribute name like this:

Name

To sort descending by budget amount, you can use the additional descqualifier:

Budget_c:desc

If you do not provide a sort direction qualifier, then an ascending sort is assumed, but you can always explicitly specify the asc qualifier, too. For example to sort descending by budget and then ascending by name, you can configure the external order by to be:

Budget_c:desc,Name:asc

External Filter & Order By for LOVs

When editing a List of Values on a Fusion Apps REST Data Source, you can use the same external where clause and external order by clause features explained above. Just look for the appropriate fields in the LOV’s Advanced REST Data Source Attributes section.

If your LOV is based on an FA REST data source with infrequently-changing lookup data, you can markedly improve performance by enabling the Use Synchronization Table switch. This will appear after you’ve configured REST Synchronization for the FA REST data source in question. Flipping this switch to the on position causes the LOV to use the locally synchronized data instead of making a REST call on each access. In this case, the External Filter and External Order By fields are no longer applicable.

If you need to apply an external filter to the REST synchronization process itself, you can do that using the p_sync_external_filter_expr parameter of the dynamic_synchronize_data() procedure in the apex_rest_source_sync package. By combining this with a scheduled automation, you can perform the REST synchronization on an appropriate schedule using your custom external filter whenever the synchronization runs against the Fusion Apps REST APIs to which the data source is related.

Using Fusion Apps REST APIs in PL/SQL

Using the procedures and functions in the apex_exec package, you can work programmatically with any REST Data Source. When working with Fusion Apps REST data sources, I like to work with the data using the business object attribute names instead of having to remember the corresponding data profile column names that apex_exec uses. The sample download at the end includes a helper package I created named oca_ds, which is short for Oracle Cloud Applications Data Source. It complements the apex_exec package to simplify working with Fusion Apps REST Data Sources programmatically.

Assuming that leads is the static id of a REST Data Source for working with the Fusion Apps leads endpoint, we can query the top 5 qualified leads with a rank of warm or hot having both a budget and decision date specified like this:

declare
   l_ctx apex_exec.t_context;
   type t_lead is record (
        name varchar(255),
        budget number,
        created timestamp with time zone);
   type t_leads is table of t_lead index by pls_integer;
   l_leads t_leads;
begin
   l_ctx := oca_ds.open_query(
               p_select   => 'Name,Budget_c,CreationDate',
               p_from     => 'leads',
               p_where    => q'~
                              StatusCode = 'QUALIFIED' and
                              Rank in ('WARM','HOT') and
                              Budget_c is not null and
                              DecisionDate_c is not null
                              ~',
               p_order_by => 'Budget_c:desc',
               p_max_rows => 5);
   while apex_exec.next_row(l_ctx) loop
      l_leads(l_leads.count+1) := 
           t_lead(oca_ds.get_varchar2(l_ctx,'Name'),
                  oca_ds.get_number(l_ctx,'Budget_c'),
                  oca_ds.get_timestamp_tz(l_ctx,'CreationDate'));
   end loop;
   oca_ds.close(l_ctx);
   -- Work with top 5 leads in l_leads here...
exception
   when others then
      oca_ds.close(l_ctx);
      raise;
end;

Inserting New Leads in PL/SQL

Of course you can use a Form or Interactive Grid region to easily insert, update, or delete data from the Leads REST data source without writing any code, but should you need to do so programmatically, it’s easy. To insert data using an FA REST data source, use the oca_ds.open_dml function to create an apex_exec.t_context object that works like a temporary rowset. Then for each new row you want to add, call oca_ds.insert_row() and then use oca_ds.set_value() to set the value of each attribute necessary. Finally, to process the pending changes call apex_exec.execute_dml().

For example, you can write the following code to insert two new leads:

declare
   l_ctx apex_exec.t_context;
begin
  -- Insert two new leads
   l_ctx := oca_ds.open_dml(
               p_attributes =>'Name,Budget_c,DecisionDate_c',
               p_from       => 'leads');
   -- Insert first lead
   oca_ds.insert_row(l_ctx);
   oca_ds.set_value(l_ctx,'Name','Remy Ratatouille');
   oca_ds.set_value(l_ctx,'DecisionDate_c',date'2024-07-31');
   oca_ds.set_value(l_ctx,'Budget_c',1800);
   -- Insert second lead
   oca_ds.insert_row(l_ctx);
   oca_ds.set_value(l_ctx,'Name','Anton Ego');
   oca_ds.set_value(l_ctx,'DecisionDate_c',date'2024-05-15');
   oca_ds.set_value(l_ctx,'Budget_c',800);
   -- Save the changes
   apex_exec.execute_dml(l_ctx);
   oca_ds.close(l_ctx);
exception
   when others then
      oca_ds.close(l_ctx);
      raise;
end;

Updating a Lead in PL/SQL

Updating data using an FA REST data source follows a similar pattern. Use the oca_ds.open_dml function to create an apex_exec.t_context object. Then for each row you want to update, call oca_ds.update_row() and then use oca_ds.set_value() to set the value of each attribute necessary. Finally, to process the pending changes call apex_exec.execute_dml(). Often you will start by querying the row first to consider its existing values first, before updating some of them. In that case you can call apex_exec.set_values() to copy all the row values from the queried row into the row to be updated.

For example, to update a lead named ‘Anton Ego’ to increase his budget by 20% and adjust his decision date to be a week (7 days) farther in the future, you can write the following code. Notice that we include the APEX$ResourceKey attribute in the list since it’s the unique identifier of the row that’s needed to perform the update.

declare
   l_ctx     apex_exec.t_context;
   l_dml_ctx apex_exec.t_context;
   -- other declarations here
begin
  -- Retrieve and update opportunity with resource key 98765
  -- to increase its amount by 20% and close date by one week
  l_ctx := oca_ds.open_query(
              p_select => 'APEX$ResourceKey,Budget_c,DecisionDate_c',
              p_from   => 'leads',
              p_where  => q'~Name='Anton Ego'~');
   if not apex_exec.next_row(l_ctx) then
      raise_application_error(-20001,'Lead Anton Ego not found');
   end if;
   l_dml_ctx := oca_ds.open_dml(
      p_from => 'leads',
      p_attributes => 'APEX$ResourceKey,Budget_c,DecisionDate_c',
      p_lost_update_detection => true);
   oca_ds.update_row(l_dml_ctx);
   -- Set the row version checksum used for lost update protection
   apex_exec.set_row_version_checksum(
      p_context   => l_dml_ctx,
      p_checksum  => apex_exec.get_row_version_checksum(l_ctx));
   -- Copy the values of the fetched lead into the update row
   apex_exec.set_values(
      p_context         => l_dml_ctx,
      p_source_context  => l_ctx );   
   -- Adjust the budget and decision date appropriately
   oca_ds.set_value(l_dml_ctx,'Budget_c', 
                    oca_ds.get_number(l_dml_ctx,'Budget_c') * 1.2);
   oca_ds.set_value(l_dml_ctx,'DecisionDate_c', 
                    oca_ds.get_date(l_dml_ctx,'DecisionDate_c') + 7);
   -- Save the changes
   apex_exec.execute_dml(l_dml_ctx);
   oca_ds.close(l_dml_ctx);
   oca_ds.close(l_ctx);
exception
   when others then
     oca_ds.close(l_dml_ctx);
     oca_ds.close(l_ctx);
     raise;
end;

Deleting a Lead in PL/SQL

Deleting data using an FA REST data source is a bit simpler. As before, use the oca_ds.open_dml function to create an apex_exec.t_context object. Then for each row you want to delete, call oca_ds.delete_row() and then use oca_ds.set_value() to set the value of the resource key APEX$ResourceKey. Finally, to process the pending changes call apex_exec.execute_dml().

Assuming the system-assigned resource key for the Anton Ego lead is 4, the following code deletes this lead:

declare
   l_dml_ctx apex_exec.t_context;
begin
  -- Delete lead with resource key '4'
   l_dml_ctx := oca_ds.open_dml(
                   p_from       => 'leads',
                   p_attributes => 'APEX$ResourceKey');
   oca_ds.delete_row(l_dml_ctx);
  -- Set the resource key of the row to be deleted
  oca_ds.set_value(l_dml_ctx,'APEX$ResourceKey', '4');
  -- Save the changes
  apex_exec.execute_dml(l_dml_ctx);
  oca_ds.close(l_dml_ctx);
exception
   when others then
     oca_ds.close(l_dml_ctx);
     raise;
end;

Finding the Logged-in User’s PartyId

As a final use case to explore, let’s imagine we are an art gallery salesperson logging in to the APEX app to work our leads for the day. Fusion Apps represents each art gallery staff member – i.e. human resource – as a Resource object in the system. When a staff member is associated with other business objects, that resource’s PartyId attribute value is used as a foreign key.

For example, a lead gets assigned to an “owner” to contact the potential customer to qualify if there is a real opportunity to sell some art. To find the list of leads owned by a particular salesperson like “Matt Hooper”, we need to know the value of Matt’s PartyId and then query the leads REST API to filter for leads where OwnerId equals the logged-in user’s PartyId value.

To simplify using the current APEX user’s PartyId in external filters on various pages of the app, we can look it up using their Username from the resources REST API after authentication and store that PartyId value in APEX session state for the duration of the user’s session. Assume we’ve created a REST Data Source of type Oracle Cloud Applications (SaaS) REST Service for the Fusion Apps resources endpoint and assigned it a static id of resources. Furthermore, assume we’ve created shared component application items named APP_USER_PARTYID and APP_USER_FULLNAME. You can write the following code in a shared component Application Process with the After Authentication process point to lookup the current user’s PartyId and PartyName from the resources REST data source and store those values in the application items:

declare
    l_ctx apex_exec.t_context;
begin
    l_ctx := oca_ds.open_query(
        p_select=> 'PartyId,PartyName',
        p_from => 'resources',
        p_where => 'upper(Username) = '||
                       apex_exec.enquote_literal(:APP_USER));
    if apex_exec.next_row(l_ctx) then
        :APP_USER_FULLNAME := oca_ds.get_varchar2(l_ctx,'PartyName'));
        :APP_USER_PARTYID  := oca_ds.get_number(l_ctx,'PartyId'));
    end if;
    oca_ds.close(l_ctx);
end set_session_info_for_user;

After setting this up, other pages in the app can reference the value of APP_USER_PARTYID in external filters using substitution syntax as we saw earlier in this article. For example, in a region based on the Leads REST data source, to filter the leads to show only ones owned by the logged-in user, we could use an external filter of:

OwnerId = &APP_USER_PARTYID.

Can Any Attribute Appear in a Filter?

A last consideration worth mentioning is that not all business object attributes can be used in a filter predicate. To discover which attributes you can use, consult the Oracle Cloud Apps Data Source Explorer utility app. There you can see if the attribute has a Y or an N in the Queryable? column. The adjective “queryable” is a bit of a misnomer. An attribute with an N in the Queryable? column can still participate in the query in the select list. However, it cannot appear in a filter predicate. For example, as shown below, the InventoryItemDescription attribute in the Leads object is not queryable.

Reviewing which attributes are Queryable in the Oracle Cloud Apps Data Source Explorer

An attempt to reference InventoryItemDescription in a filter predicate results in the error:

Search filter includes a non-queryable column or unsupported operator

To install the Oracle Cloud Apps Data Source Explorer app visit the Gallery > Utility Apps tab and click the Install button like you’ve done for other sample apps. It helps you explore interesting information about all the Fusion Apps REST APIs your applications use.

Downloading the Helper Package

Hopefully, this information about Fusion Apps REST API filters and how to use them will come in handy the next time you create an APEX application that works with these powerful business object APIs. You can download the source of my oca_ds helper package here. For more information, see the Working with REST Data Sources for Oracle Cloud SaaS Applications section of the APEX developer documentation. It explains all the features for working easily with Fusion Applications REST APIs.