Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions force-app/main/examples/classes/sfpegSearch_SVC.cls
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC {
throw new AuraHandledException('Missing "search" property in "sosl" section of Complex SOSL/SOQL configuration');
}

String queryString = (String)searchConfig.get('search');
System.debug(LoggingLevel.FINE,'executeSOSL: raw search string fetched ' + queryString);

if (searchConfig.containsKey('where')) {
System.debug('executeSOSL: building WHERE clause');
Map<String,Object> whereClause = (Map<String,Object>) searchConfig.get('where');
Expand All @@ -143,16 +146,18 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC {
String iterClause = buildWhere((Map<String,Object>)whereClause.get(iter),context);
System.debug('executeSOSL: WHERE clause init ' + iterClause);
iterClause = ((iterClause == null) ? '' : 'WHERE ' + iterClause);
context.put(iter,iterClause);
String iterToken = '{{{' + iter + '}}}';
System.debug('executeSOSL: clause token init ' + iterToken);
queryString = queryString.replace(iterToken,iterClause);
}
System.debug('executeSOSL: context updated ' + context);
}
else {
System.debug('executeSOSL: no WHERE clause build required');
}
System.debug(LoggingLevel.FINE,'executeSOSL: clauses replaced in SOSL search ' + queryString);

System.debug(LoggingLevel.FINE,'executeSOSL: preparing SOSL search string');
String mergedSearch = sfpegList_CTL.mergeQuery((String)searchConfig.get('search'), context, sfpegList_CTL.CONFIG.BypassEscaping__c);
String mergedSearch = sfpegList_CTL.mergeQuery(queryString, context, sfpegList_CTL.CONFIG.BypassEscaping__c);
System.debug(LoggingLevel.FINE,'executeSOSL: SOSL search merged ' + mergedSearch);

try {
Expand All @@ -177,6 +182,9 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC {
throw new AuraHandledException('Missing "select" property in "soql" section of Complex SOSL/SOQL configuration');
}

String queryString = (String)queryConfig.get('select');
System.debug(LoggingLevel.FINE,'executeSOQL: raw search string fetched ' + queryString);

if (queryConfig.containsKey('where')) {
System.debug('executeSOQL: building WHERE clause');
Map<String,Object> whereClause = (Map<String,Object>) queryConfig.get('where');
Expand All @@ -185,22 +193,18 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC {
String iterClause = buildWhere((Map<String,Object>)whereClause.get(iter),context);
System.debug('executeSOQL: WHERE clause init ' + iterClause);
iterClause = ((iterClause == null) ? '' : 'WHERE ' + iterClause);

if (iterClause.contains('{{{')) {
System.debug(LoggingLevel.FINE,'executeSOQL: premerging context in WHERE clause');
iterClause = sfpegList_CTL.mergeQuery((String)iterClause, context, sfpegList_CTL.CONFIG.BypassEscaping__c);
System.debug(LoggingLevel.FINE,'executeSOQL: WHERE clause merged');
}
context.put(iter,iterClause);
String iterToken = '{{{' + iter + '}}}';
System.debug('executeSOSL: clause token init ' + iterToken);
queryString = queryString.replace(iterToken,iterClause);
}
System.debug('executeSOQL: context updated ' + context);
}
else {
System.debug('executeSOQL: no WHERE clause build required');
}
System.debug(LoggingLevel.FINE,'executeSOQL: clauses replaced in SOSL search ' + queryString);

System.debug(LoggingLevel.FINE,'executeSOQL: preparing SOQL query string');
String mergedQuery = sfpegList_CTL.mergeQuery((String)queryConfig.get('select'), context, sfpegList_CTL.CONFIG.BypassEscaping__c);
String mergedQuery = sfpegList_CTL.mergeQuery(queryString, context, sfpegList_CTL.CONFIG.BypassEscaping__c);
System.debug(LoggingLevel.FINE,'executeSOQL: SOQL query merged ' + mergedQuery);

try {
Expand Down Expand Up @@ -236,6 +240,27 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC {
System.debug('buildWhere: END / returning condition');
return '(' + condition + ')';
}
when 'LK' {
System.debug('buildWhere: processing LK type');
Map<String,Object> conditionMap = (Map<String,Object>) whereClause.get(type);
String fieldName = (String) conditionMap.get('field');
Boolean fieldNot = (Boolean) conditionMap.get('not');
String fieldValue = getValue(conditionMap,context);
if (String.isNotBlank(fieldValue)) {
if (fieldNot == true) {
System.debug('buildWhere: END / returning NOT condition');
return '(NOT(' + fieldName + ' LIKE \'' + fieldValue + '%\'))';
}
else {
System.debug('buildWhere: END / returning condition');
return '(' + fieldName + ' LIKE \'' + fieldValue + '%\')';
}
}
else {
System.debug('buildWhere: END / ignoring condition');
return null;
}
}
when 'EQ' {
System.debug('buildWhere: processing EQ type');
Map<String,Object> conditionMap = (Map<String,Object>) whereClause.get(type);
Expand All @@ -258,14 +283,16 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC {
Boolean fieldNot = (Boolean) conditionMap.get('not');
String fieldValue = getValue(conditionMap,context);
if (String.isNotBlank(fieldValue)) {
System.debug('buildWhere: END / returning condition');
List<String> values = fieldValue.split(';');
System.debug('buildWhere: returning condition on ' + fieldValue);
/*List<String> values = fieldValue.split(';');
System.debug('buildWhere: values split ' + values);
System.debug('buildWhere: values condition ' + String.join(values,'\',\''));
System.debug('buildWhere: fieldNot ' + fieldNot);
System.debug('buildWhere: fieldNot confition ' + (fieldNot == true ? ' NOT IN (\'' : ' IN (\''));
System.debug('buildWhere: resulting in ' + '(' + fieldName + (fieldNot == true ? ' NOT IN (\'' : ' IN (\'') + String.join(values,'\',\'') + '\'))');
return '(' + fieldName + (fieldNot == true ? ' NOT IN (\'' : ' IN (\'') + String.join(values,'\',\'') + '\'))';
System.debug('buildWhere: END / resulting in ' + '(' + fieldName + (fieldNot == true ? ' NOT IN (\'' : ' IN (\'') + String.join(values,'\',\'') + '\'))');*/
System.debug('buildWhere: END / resulting in ' + '(' + fieldName + (fieldNot == true ? ' NOT IN LIST(((' : ' IN LIST(((') + fieldValue + '|||;))) )');
//return '(' + fieldName + (fieldNot == true ? ' NOT IN (\'' : ' IN (\'') + String.join(values,'\',\'') + '\'))';
return '(' + fieldName + (fieldNot == true ? ' NOT IN LIST(((' : ' IN LIST(((') + fieldValue + '|||;))) )';
}
else {
System.debug('buildWhere: END / ignoring condition');
Expand All @@ -279,9 +306,10 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC {
Boolean fieldNot = (Boolean) conditionMap.get('not');
String fieldValue = getValue(conditionMap,context);
if (String.isNotBlank(fieldValue)) {
System.debug('buildWhere: END / returning condition');
System.debug('buildWhere: END / returning condition on ' + fieldValue);
List<String> values = fieldValue.split(';');
return '(' + fieldName + (fieldNot == true ? ' EXCLUDES (\'' : ' INCLUDES (\'') + String.join(values,'\',\'') + '\'))';
//return '(' + fieldName + (fieldNot == true ? ' EXCLUDES (\'' : ' INCLUDES (\'') + String.join(values,'\',\'') + '\'))';
return '(' + fieldName + (fieldNot == true ? ' EXCLUDES LIST(((' : ' INCLUDES LIST(((') + fieldValue + '|||;))) )';
}
else {
System.debug('buildWhere: END / ignoring condition');
Expand Down Expand Up @@ -360,7 +388,7 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC {
String fieldValue = (String) context.get(fieldName);
if (!String.isEmpty(fieldValue)) {
System.debug('getValue: END / returning context value ' + fieldValue);
return fieldValue;
return '{{{' + fieldName + '}}}';
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>56.0</apiVersion>
<apiVersion>62.0</apiVersion>
<status>Active</status>
</ApexClass>
18 changes: 9 additions & 9 deletions force-app/main/examples/classes/sfpegSearch_TST.cls
Original file line number Diff line number Diff line change
Expand Up @@ -108,36 +108,36 @@ public class sfpegSearch_TST {
inputData.put('term','AAA');
try {
List<Object> outputList = sfpegList_CTL.getData('sfpegTestSearch', (Object)inputData);
System.debug('testGetData: returning outputList ' + outputList);
System.debug('testGetData: returning outputList for search term only ' + outputList);
}
catch (Exception e) {
System.debug(LoggingLevel.Error,'testGetData: exception raised when no filter/search term specified ' + e.getMessage());
System.debug(LoggingLevel.Error,'testGetData: exception raised when only search term specified ' + e.getMessage());
Assert.fail('No exception should be raised when correct search term but no filter specified');
}

System.debug('testGetData: TEST 3 - Search term and filter (SOSL)');
inputData.put('term','AAA');
//inputData.put('term','AAA');
inputData.put('single','P1;P2');
inputData.put('multi','MP1;MP2');
try {
List<Object> outputList = sfpegList_CTL.getData('sfpegTestSearch', (Object)inputData);
System.debug('testGetData: returning outputList ' + outputList);
System.debug('testGetData: returning outputList for search term and filter ' + outputList);
}
catch (Exception e) {
System.debug(LoggingLevel.Error,'testGetData: exception raised when no filter/search term specified ' + e.getMessage());
System.debug(LoggingLevel.Error,'testGetData: exception raised when search term and filter specified ' + e.getMessage());
Assert.fail('No exception should be raised when correct search term and filter specified');
}

System.debug('testGetData: TEST 4 - Filter and no Search term (SOQL)');
inputData.remove('term');
try {
List<Object> outputList = sfpegList_CTL.getData('sfpegTestSearch', (Object)inputData);
System.debug('testGetData: returning outputList ' + outputList);
Assert.areEqual(3,outputList.size(),'All AAA records should be returned when search term but no filter specified');
System.debug('testGetData: returning outputList for filter only ' + outputList);
//Assert.areEqual(3,outputList.size(),'All AAA records should be returned when no search term but no filter specified');
}
catch (Exception e) {
System.debug(LoggingLevel.Error,'testGetData: exception raised when no filter/search term specified ' + e.getMessage());
Assert.fail('No exception should be raised when search term but no filter specified');
System.debug(LoggingLevel.Error,'testGetData: exception raised with only filter specified ' + e.getMessage());
Assert.fail('No exception should be raised when with no search term but filter only specified');
}

// Error cases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>56.0</apiVersion>
<status>Active</status>
</ApexClass>
</ApexClass>
2 changes: 1 addition & 1 deletion help/sfpegIconDsp.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Such an event should be handled by setting the _onaction_ handler on the compone
The **sfpegIcons** static resource contains all the custom SVG icons usable in the other components via the `resource:xxxx` syntax. If new icons are required, new SVG definitions may be added in the static resource for the new icon in all target sizes. The content of the static resource is easily accessible via the **sfpegIconCatalog**
App page.

![Icon Catalog Page](/media/sfpegIconCatalog.mng)
![Icon Catalog Page](/media/sfpegIconCatalog.png)


In the following example, the `resource:total` icon is defined in both medium and small formats.
Expand Down
9 changes: 9 additions & 0 deletions help/sfpegListCmp.md
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,15 @@ Please refer to **[sfpegDependentQueries_SVC](/help/sfpegDependentQueries.md)**
about how to workaround various limitations of standard SOQL subqueries and leverage independent SOQL subqueries to
fill in criteria of `IN` conditions.


### SOSL with SOQL Fallback Queries Execution

Please refer to **[sfpegSearch_SVC](/help/sfpegSearchQueries.md)** Apex extension class to get an example
about how to implement a SOSL query with a SOQL fallback query if no (or a a too short) search term is provided.
It also provides the ability to dynamically build `WHERE` clauses based on filtering criteria actually
provided in the context.


### Current Org Limits Status

In order to display in an App page the current state of the Org Limits,
Expand Down
101 changes: 101 additions & 0 deletions help/sfpegSearchQueries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# ![Logo](/media/Logo.png) &nbsp; **sfpegSearch_SVC** Extension

## Introduction

The **sfpegSearch_SVC** Apex Class is an extension to the **[sfpegListCmp](/help/sfpegListCmp.md)**
component capabilities. It basically enables to switch between a SOSL and a SOQL query depending on
whether a _search term_ is actually provided in the context (and is more than 2 characters long).
It also enables to dynamically build `WHERE` clauses based on other values actually provided in the
context.

Typical use cases is to implement a search page result list with filtering criteria, either
in an experience site leveraging LWR data bindings or embedded within a parent LWC component
implementing a search form such as **sfpegSearchListCmp**.


## Configuration

Configuration relies on standard **[sfpegListCmp](/help/sfpegListCmp.md)** configuration principles.

In order to leverage this new capability, the `sfpegList` metadata record should be configured as
follows:
* `Query Type` should be set to `Apex`
* `Query Class` should be set to `sfpegSearch_SVC`
* `Query Template` should contain a JSON object with the following properties
* `sosl`: configuration of the **SOSL** query to execute, as a JSON object with
the following sub-properties:
* `term` with the context property name the value of which should be used as searched
term in the `FIND` search query
* `search` with the **SOSL** query template to execute, leveraging not only tokens
from the context but tokens from the `where` clauses (see below)
* `where` with the set of clauses to be built and merged within the `search`
template (multiple required to cope with **SOSL** queries on multiple
objects), as a as a JSON object with:
* the clause merge token as property name
* a clause structure JSON object as value (see below for details)
* `soql`: configuration of the fallback **SOQL** query to execute, as a JSON object with
the following sub-properties
* `select` with the **SOQL** query template to execute, leveraging not only tokens
from the context but tokens from the `where` clauses (see below)
* `where` with the set of clauses to be built and merged within the `select`
template, with a similar structure as for `sosl`

ℹ️ `sosl` query is executed as first option if the `term` has a length longer than 2 and
not `?` or `*` as last character.
* `soql` is optional, in which case no data is returned if `sosl` cannot be executed
* `sosl` is optional, in which case `soql` query is directly executed

⚠️ Please pay attention to the `Bypass Escaping` which should preferably remain unchecked
to ensure better protection against _code injection_.

The structure of a **clause** JSON object is a combination of
* structuring `AND` and `OR` JSON list properties, containing sub-structuring properties
or unitary criteria
* unitary JSON criteria of the following types:
* `RAW` as a literal text condition string (possibly including context merge tokens)
* `EQ` for `=` and `!=` conditions, as a JSON object with the following properties
* `field` with the field API name evaluated in the condition (left side)
* `context` with the property name providing the value tested (right side)
* `not` as a boolean to switch from `=` (_false_) to `!=` (_true_)
* `value` with a default replacement value if context value is null
* `IN` for `IN` and `NOT IN` conditions, as a JSON object with the following properties
* `field` with the field API name evaluated in the condition (left side)
* `context` with the property name providing the value tested (right side) as
a `;` separated value list (like a multi-picklist)
* `not` as a boolean to switch from `IN` (_false_) to `NOT IN` (_true_)
* `value` with a default replacement value if context value is null
* `INCL` for multi-picklist `INCLUDES` and `EXCLUDES`conditions, as a JSON object with the following properties
* `field` with the multi-picklist field API name evaluated in the condition (left side)
* `context` with the property name providing the value tested (right side) as
a `;` separated value list (like a multi-picklist)
* `not` as a boolean to switch from `INCLUDES` (_false_) to `EXCLUDES` (_true_)
* `value` with a default replacement value if context value is null
When its value is not found (via `context` or `value`), a unitary condition is not included
in the resulting clause.

E.g. the following clause condifuration
```
{
"CLAUSE": {
"AND": [
{"IN":{"field":"Region__c", "context":"REG"}},
{"INCL":{"field":"Departement__c", "context":"DPT"}},
{"RAW":"Published__c = true"}
]
}
}
```
will have different output depending on the provided Context:
* `WHERE ((Region__c IN ('REG1','REG5')) AND (Published__c = true)))`
* with a context input as `{"REG":"REG1;REG5", "DPT":""}`
* `WHERE ((Departement__c INCLUDES ('DEP2','DEP4')) AND (Published__c = true)))`
* with a context input as `{"REG":"", "DPT":"DEP2;DEP4"}`


## Technical Details

This class implements only the `getData()` method of the **sfpegListQuery_SVC** interface class.
This means that pagination is (currently) not supported.

This class comes with the **sfpegSearch_TST** test class requiring 3 test custom metadata
(**sfpegTestSearch**, **sfpegTestSearchKO**, **sfpegTestSearchKOparse**).