This is an automated email from the ASF dual-hosted git repository.

pvillard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 35219052e1 NIFI-13869: Enhance QuerySalesforceObject Processor to 
Support Querying Deleted Records
35219052e1 is described below

commit 35219052e13c9a2a2f3a40e125832a872067d1b1
Author: Nicolae Puica <[email protected]>
AuthorDate: Tue Oct 22 14:46:47 2024 +0200

    NIFI-13869: Enhance QuerySalesforceObject Processor to Support Querying 
Deleted Records
    
    - Added a new boolean property `Include Deleted Records` to allow users to 
include deleted (soft-deleted) records in Salesforce queries.
    - Modified the query construction to include the `IsDeleted` field and use 
the `queryAll` API endpoint when `Include Deleted Records` is enabled.
    - Updated `SalesforceRestClient` to support the `queryAll` API by adding a 
new `queryAll` method.
    - Ensured backward compatibility by defaulting `Include Deleted Records` to 
`false`, maintaining existing behavior unless explicitly enabled.
    - Updated processor documentation and annotations to reflect the new 
functionality.
    - Adjusted state management to reset state when `Include Deleted Records` 
property is modified.
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #9434.
---
 .../salesforce/QuerySalesforceObject.java          | 52 +++++++++++++++++-----
 .../salesforce/rest/SalesforceRestClient.java      |  8 ++++
 2 files changed, 48 insertions(+), 12 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-salesforce-bundle/nifi-salesforce-processors/src/main/java/org/apache/nifi/processors/salesforce/QuerySalesforceObject.java
 
b/nifi-extension-bundles/nifi-salesforce-bundle/nifi-salesforce-processors/src/main/java/org/apache/nifi/processors/salesforce/QuerySalesforceObject.java
index 9906bbee22..82e8b54b37 100644
--- 
a/nifi-extension-bundles/nifi-salesforce-bundle/nifi-salesforce-processors/src/main/java/org/apache/nifi/processors/salesforce/QuerySalesforceObject.java
+++ 
b/nifi-extension-bundles/nifi-salesforce-bundle/nifi-salesforce-processors/src/main/java/org/apache/nifi/processors/salesforce/QuerySalesforceObject.java
@@ -111,7 +111,9 @@ import static 
org.apache.nifi.processors.salesforce.util.CommonSalesforcePropert
         + " It's also possible to define an initial cutoff value for the age, 
filtering out all older records"
         + " even for the first run. In case of 'Property Based Query' this 
processor should run on the Primary Node only."
         + " FlowFile attribute 'record.count' indicates how many records were 
retrieved and written to the output."
-        + " The processor can accept an optional input FlowFile and reference 
the FlowFile attributes in the query.")
+        + " The processor can accept an optional input FlowFile and reference 
the FlowFile attributes in the query."
+        + " When 'Include Deleted Records' is true, the processor will include 
deleted records (soft-deletes) in the results by using the 'queryAll' API."
+        + " The 'IsDeleted' field will be automatically included in the 
results when querying deleted records.")
 @Stateful(scopes = Scope.CLUSTER, description = "When 'Age Field' is set, 
after performing a query the time of execution is stored. Subsequent queries 
will be augmented"
         + " with an additional condition so that only records that are newer 
than the stored execution time (adjusted with the optional value of 'Age 
Delay') will be retrieved."
         + " State is stored across the cluster so that this Processor can be 
run on Primary Node only and if a new Primary Node is selected,"
@@ -232,6 +234,16 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
             .dependsOn(QUERY_TYPE, PROPERTY_BASED_QUERY)
             .build();
 
+    static final PropertyDescriptor INCLUDE_DELETED_RECORDS = new 
PropertyDescriptor.Builder()
+            .name("include-deleted-records")
+            .displayName("Include Deleted Records")
+            .description("If true, the processor will include deleted records 
(IsDeleted = true) in the query results. When enabled, the processor will use 
the 'queryAll' API.")
+            .required(true)
+            .defaultValue("false")
+            .allowableValues("true", "false")
+            .dependsOn(QUERY_TYPE, PROPERTY_BASED_QUERY)
+            .build();
+
     static final Relationship REL_SUCCESS = new Relationship.Builder()
             .name("success")
             .description("For FlowFiles created as a result of a successful 
query.")
@@ -306,6 +318,7 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
             INITIAL_AGE_FILTER,
             AGE_DELAY,
             CUSTOM_WHERE_CONDITION,
+            INCLUDE_DELETED_RECORDS,
             READ_TIMEOUT,
             CREATE_ZERO_RECORD_FILES,
             TOKEN_PROVIDER
@@ -339,7 +352,8 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
                 || descriptor.equals(SOBJECT_NAME)
                 || descriptor.equals(AGE_FIELD)
                 || descriptor.equals(INITIAL_AGE_FILTER)
-                || descriptor.equals(CUSTOM_WHERE_CONDITION))
+                || descriptor.equals(CUSTOM_WHERE_CONDITION)
+                || descriptor.equals(INCLUDE_DELETED_RECORDS))
         ) {
             getLogger().debug("A property that require resetting state was 
modified - {} oldValue {} newValue {}",
                     descriptor.getDisplayName(), oldValue, newValue);
@@ -365,10 +379,11 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
         String customWhereClause = 
context.getProperty(CUSTOM_WHERE_CONDITION).evaluateAttributeExpressions(originalFlowFile).getValue();
         RecordSetWriterFactory writerFactory = 
context.getProperty(RECORD_WRITER).asControllerService(RecordSetWriterFactory.class);
         boolean createZeroRecordFlowFiles = 
context.getProperty(CREATE_ZERO_RECORD_FILES).asBoolean();
+        boolean includeDeletedRecords = 
context.getProperty(INCLUDE_DELETED_RECORDS).asBoolean();
 
         StateMap state = getState(session);
         IncrementalContext incrementalContext = new 
IncrementalContext(context, state);
-        SalesforceSchemaHolder salesForceSchemaHolder = 
getConvertedSalesforceSchema(sObject, fields);
+        SalesforceSchemaHolder salesForceSchemaHolder = 
getConvertedSalesforceSchema(sObject, fields, includeDeletedRecords);
 
         if (StringUtils.isBlank(fields)) {
             fields = salesForceSchemaHolder.getSalesforceObject().getFields()
@@ -377,6 +392,14 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
                     .collect(Collectors.joining(","));
         }
 
+        // Add IsDeleted to fields if Include Deleted Records is true
+        if (includeDeletedRecords) {
+            List<String> fieldList = 
Arrays.stream(fields.split("\\s*,\\s*")).collect(Collectors.toList());
+            if (fieldList.stream().noneMatch(f -> 
f.equalsIgnoreCase("IsDeleted"))) {
+                fields = fields + ", IsDeleted";
+            }
+        }
+
         String querySObject = new SalesforceQueryBuilder(incrementalContext)
                 .buildQuery(sObject, fields, customWhereClause);
 
@@ -396,7 +419,7 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
             AtomicInteger recordCountHolder = new AtomicInteger();
             try {
                 outgoingFlowFile = session.write(outgoingFlowFile, 
processRecordsCallback(session, nextRecordsUrl, writerFactory, state, 
incrementalContext,
-                        salesForceSchemaHolder, querySObject, 
originalAttributes, attributes, recordCountHolder));
+                        salesForceSchemaHolder, querySObject, 
originalAttributes, attributes, recordCountHolder, includeDeletedRecords));
                 int recordCount = recordCountHolder.get();
 
                 if (createZeroRecordFlowFiles || recordCount != 0) {
@@ -428,10 +451,10 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
     private OutputStreamCallback processRecordsCallback(ProcessSession 
session, AtomicReference<String> nextRecordsUrl, RecordSetWriterFactory 
writerFactory,
                                                         StateMap state, 
IncrementalContext incrementalContext, SalesforceSchemaHolder 
salesForceSchemaHolder,
                                                         String querySObject, 
Map<String, String> originalAttributes, Map<String, String> attributes,
-                                                        AtomicInteger 
recordCountHolder) {
+                                                        AtomicInteger 
recordCountHolder, boolean includeDeletedRecords) {
         return out -> {
             try {
-                handleRecordSet(out, nextRecordsUrl, querySObject, 
writerFactory, salesForceSchemaHolder, originalAttributes, attributes, 
recordCountHolder);
+                handleRecordSet(out, nextRecordsUrl, querySObject, 
writerFactory, salesForceSchemaHolder, originalAttributes, attributes, 
recordCountHolder, includeDeletedRecords);
 
                 if (incrementalContext.getAgeFilterUpper() != null) {
                     Map<String, String> newState = new 
HashMap<>(state.toMap());
@@ -446,9 +469,9 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
 
     private void handleRecordSet(OutputStream out, AtomicReference<String> 
nextRecordsUrl, String querySObject, RecordSetWriterFactory writerFactory,
                                  SalesforceSchemaHolder 
salesForceSchemaHolder, Map<String, String> originalAttributes, Map<String, 
String> attributes,
-                                 AtomicInteger recordCountHolder) throws 
Exception {
+                                 AtomicInteger recordCountHolder, boolean 
includeDeletedRecords) throws Exception {
         try (
-                InputStream querySObjectResultInputStream = 
getResultInputStream(nextRecordsUrl.get(), querySObject);
+                InputStream querySObjectResultInputStream = 
getResultInputStream(nextRecordsUrl.get(), querySObject, includeDeletedRecords);
                 JsonTreeRowRecordReader jsonReader = 
createJsonReader(querySObjectResultInputStream, 
salesForceSchemaHolder.getRecordSchema());
                 RecordSetWriter writer = createRecordSetWriter(writerFactory, 
originalAttributes, out, salesForceSchemaHolder.getRecordSchema())
         ) {
@@ -506,8 +529,9 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
         AtomicBoolean isOriginalTransferred = new AtomicBoolean(false);
         List<FlowFile> outgoingFlowFiles = new ArrayList<>();
         long startNanos = System.nanoTime();
+        boolean includeDeletedRecords = 
context.getProperty(INCLUDE_DELETED_RECORDS).asBoolean();
         do {
-            try (InputStream response = 
getResultInputStream(nextRecordsUrl.get(), customQuery)) {
+            try (InputStream response = 
getResultInputStream(nextRecordsUrl.get(), customQuery, includeDeletedRecords)) 
{
                 FlowFile outgoingFlowFile = createOutgoingFlowFile(session, 
originalFlowFile);
                 outgoingFlowFiles.add(outgoingFlowFile);
                 outgoingFlowFile = session.write(outgoingFlowFile, 
parseCustomQueryResponse(response, nextRecordsUrl, totalSize));
@@ -574,14 +598,18 @@ public class QuerySalesforceObject extends 
AbstractProcessor {
                 .equals(value) && jsonParser.nextToken() != null;
     }
 
-    private InputStream getResultInputStream(String nextRecordsUrl, String 
querySObject) {
+    private InputStream getResultInputStream(String nextRecordsUrl, String 
querySObject, boolean includeDeletedRecords) {
         if (nextRecordsUrl == null) {
-            return salesforceRestService.query(querySObject);
+            if (includeDeletedRecords) {
+                return salesforceRestService.queryAll(querySObject);
+            } else {
+                return salesforceRestService.query(querySObject);
+            }
         }
         return salesforceRestService.getNextRecords(nextRecordsUrl);
     }
 
-    private SalesforceSchemaHolder getConvertedSalesforceSchema(String 
sObject, String fields) {
+    private SalesforceSchemaHolder getConvertedSalesforceSchema(String 
sObject, String fields, boolean includeDeletedRecords) {
         try (InputStream describeSObjectResult = 
salesforceRestService.describeSObject(sObject)) {
             return convertSchema(describeSObjectResult, fields);
         } catch (IOException e) {
diff --git 
a/nifi-extension-bundles/nifi-salesforce-bundle/nifi-salesforce-processors/src/main/java/org/apache/nifi/processors/salesforce/rest/SalesforceRestClient.java
 
b/nifi-extension-bundles/nifi-salesforce-bundle/nifi-salesforce-processors/src/main/java/org/apache/nifi/processors/salesforce/rest/SalesforceRestClient.java
index 26cb6c420a..5fd0ff326d 100644
--- 
a/nifi-extension-bundles/nifi-salesforce-bundle/nifi-salesforce-processors/src/main/java/org/apache/nifi/processors/salesforce/rest/SalesforceRestClient.java
+++ 
b/nifi-extension-bundles/nifi-salesforce-bundle/nifi-salesforce-processors/src/main/java/org/apache/nifi/processors/salesforce/rest/SalesforceRestClient.java
@@ -57,6 +57,14 @@ public class SalesforceRestClient {
         return executeRequest(request);
     }
 
+    public InputStream queryAll(String query) {
+        HttpUrl httpUrl = HttpUrl.get(getUrl("/queryAll")).newBuilder()
+                .addQueryParameter("q", query)
+                .build();
+        Request request = buildGetRequest(httpUrl.toString());
+        return executeRequest(request);
+    }
+
     public InputStream getNextRecords(String nextRecordsUrl) {
         HttpUrl httpUrl = HttpUrl.get(configuration.getInstanceUrl() + 
nextRecordsUrl).newBuilder().build();
         Request request = buildGetRequest(httpUrl.toString());

Reply via email to