From bdb25d33a27a81a0f2e291667d9841008140c0a2 Mon Sep 17 00:00:00 2001 From: P-E Gros <“pgros@salesforce.com”> Date: Tue, 4 Feb 2025 13:45:45 +0100 Subject: [PATCH 1/5] Issue #79 fixed on sfpegSearch_SVC --- .../main/examples/classes/sfpegSearch_SVC.cls | 51 ++++++--- .../classes/sfpegSearch_SVC.cls-meta.xml | 2 +- .../main/examples/classes/sfpegSearch_TST.cls | 18 ++-- .../classes/sfpegSearch_TST.cls-meta.xml | 2 +- help/sfpegListCmp.md | 9 ++ help/sfpegSearchQueries.md | 101 ++++++++++++++++++ 6 files changed, 159 insertions(+), 24 deletions(-) create mode 100644 help/sfpegSearchQueries.md diff --git a/force-app/main/examples/classes/sfpegSearch_SVC.cls b/force-app/main/examples/classes/sfpegSearch_SVC.cls index 2e6b75f..6ddafa4 100644 --- a/force-app/main/examples/classes/sfpegSearch_SVC.cls +++ b/force-app/main/examples/classes/sfpegSearch_SVC.cls @@ -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 whereClause = (Map) searchConfig.get('where'); @@ -143,16 +146,18 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC { String iterClause = buildWhere((Map)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 { @@ -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 whereClause = (Map) queryConfig.get('where'); @@ -185,22 +193,18 @@ public with sharing class sfpegSearch_SVC extends sfpegListQuery_SVC { String iterClause = buildWhere((Map)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 { @@ -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 conditionMap = (Map) 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 conditionMap = (Map) whereClause.get(type); @@ -360,7 +385,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 + '}}}'; } } diff --git a/force-app/main/examples/classes/sfpegSearch_SVC.cls-meta.xml b/force-app/main/examples/classes/sfpegSearch_SVC.cls-meta.xml index fbbad0a..998805a 100644 --- a/force-app/main/examples/classes/sfpegSearch_SVC.cls-meta.xml +++ b/force-app/main/examples/classes/sfpegSearch_SVC.cls-meta.xml @@ -1,5 +1,5 @@ - 56.0 + 62.0 Active diff --git a/force-app/main/examples/classes/sfpegSearch_TST.cls b/force-app/main/examples/classes/sfpegSearch_TST.cls index 6dfa698..ac94aeb 100644 --- a/force-app/main/examples/classes/sfpegSearch_TST.cls +++ b/force-app/main/examples/classes/sfpegSearch_TST.cls @@ -108,23 +108,23 @@ public class sfpegSearch_TST { inputData.put('term','AAA'); try { List 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 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'); } @@ -132,12 +132,12 @@ public class sfpegSearch_TST { inputData.remove('term'); try { List 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 diff --git a/force-app/main/examples/classes/sfpegSearch_TST.cls-meta.xml b/force-app/main/examples/classes/sfpegSearch_TST.cls-meta.xml index 9bbf7b4..fbbad0a 100644 --- a/force-app/main/examples/classes/sfpegSearch_TST.cls-meta.xml +++ b/force-app/main/examples/classes/sfpegSearch_TST.cls-meta.xml @@ -2,4 +2,4 @@ 56.0 Active - \ No newline at end of file + diff --git a/help/sfpegListCmp.md b/help/sfpegListCmp.md index a776f34..c18b12a 100755 --- a/help/sfpegListCmp.md +++ b/help/sfpegListCmp.md @@ -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, diff --git a/help/sfpegSearchQueries.md b/help/sfpegSearchQueries.md new file mode 100644 index 0000000..23aa02c --- /dev/null +++ b/help/sfpegSearchQueries.md @@ -0,0 +1,101 @@ +# ![Logo](/media/Logo.png)   **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**). \ No newline at end of file From 4e14cac8acddb5e41cbfd0f517d68903d2560e75 Mon Sep 17 00:00:00 2001 From: P-E Gros Date: Fri, 7 Mar 2025 10:15:56 +0100 Subject: [PATCH 2/5] Additional fix for Issue #79 for IN and INCL conditions --- .../main/examples/classes/sfpegSearch_SVC.cls | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/force-app/main/examples/classes/sfpegSearch_SVC.cls b/force-app/main/examples/classes/sfpegSearch_SVC.cls index 6ddafa4..5070de1 100644 --- a/force-app/main/examples/classes/sfpegSearch_SVC.cls +++ b/force-app/main/examples/classes/sfpegSearch_SVC.cls @@ -283,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 values = fieldValue.split(';'); + System.debug('buildWhere: returning condition on ' + fieldValue); + /*List 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'); @@ -304,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 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'); From e9dfa1fe1b7b8cd4d1ac37f9742a9cdfe65148a8 Mon Sep 17 00:00:00 2001 From: Pierre-Emmanuel GROS Date: Fri, 1 Aug 2025 15:23:07 +0200 Subject: [PATCH 3/5] Update sfpegIconDsp.md minor doc fix --- help/sfpegIconDsp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/help/sfpegIconDsp.md b/help/sfpegIconDsp.md index ba1e264..20de8d1 100755 --- a/help/sfpegIconDsp.md +++ b/help/sfpegIconDsp.md @@ -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. From 93224b9170896947143bf2e9f771593117615415 Mon Sep 17 00:00:00 2001 From: Satveer Bhantoo Date: Fri, 12 Sep 2025 09:51:58 +0400 Subject: [PATCH 4/5] Patching component display after regression from winter 25 release --- force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html b/force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html index 0f4e39f..5b7348c 100755 --- a/force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html +++ b/force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html @@ -125,7 +125,7 @@
  • From af4cc0080eee9c4a5b3eb1a1daa82dd1272255e3 Mon Sep 17 00:00:00 2001 From: Satveer Bhantoo Date: Mon, 29 Sep 2025 11:16:02 +0400 Subject: [PATCH 5/5] revert back commit on winter 25 regression --- force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html b/force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html index 5b7348c..0f4e39f 100755 --- a/force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html +++ b/force-app/main/default/lwc/sfpegKpiListCmp/sfpegKpiListCmp.html @@ -125,7 +125,7 @@