Skip to content

Fix: Resolve reviewer feedback on Exporting Salesforce Leads to Google Sheets integration sample - #73

Open
minuraashen wants to merge 2 commits into
wso2:mainfrom
minuraashen:main
Open

Fix: Resolve reviewer feedback on Exporting Salesforce Leads to Google Sheets integration sample#73
minuraashen wants to merge 2 commits into
wso2:mainfrom
minuraashen:main

Conversation

@minuraashen

@minuraashen minuraashen commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Purpose

Resolves reviewer feedback on the Exporting Salesforce Leads to Google Sheets integration sample PR. This addresses the logic and functionality issues identified during the review process.

Goals

  • Fix logic and functionality bugs identified by the reviewer
  • Ensure the Salesforce Leads to Google Sheets integration sample works as expected
  • Improve the overall quality and correctness of the integration sample

Approach

Implemented fixes based on reviewer feedback:

  • Addressed logic and functionality issues in the Ballerina integration code

User stories

As a sales operations user, I want new Salesforce leads to automatically appear in Google Sheets so that I can track and analyze leads without manual data entry.

Release note

Bug fix: Resolved logic and functionality issues in the Salesforce Leads to Google Sheets prebuilt integration sample based on reviewer feedback.

Documentation

N/A — No documentation changes required. Existing README.md remains valid.

Training

N/A

Certification

N/A — No new features introduced; this is a bug fix based on reviewer feedback.

Marketing

N/A

Automation tests

  • Unit tests

    No changes to unit test coverage

  • Integration tests

    Re-tested end-to-end with Salesforce Developer Account and Google Sheets API after applying fixes

Security checks

Samples

This PR updates the existing sample — Export Salesforce Leads to Google Sheets — fixing logic and functionality issues to ensure correct behavior when:

  • Listening for new lead creation events from Salesforce
  • Appending lead details (name, email, company, etc.) to a Google Sheet automatically

Related PRs

Original PR: #61 (merged)

Migrations (if applicable)

N/A

Test environment

  • OS: macOS
  • Ballerina version: 2201.13.1
  • Tested with Salesforce Developer Account and Google Sheets API

Learning

  • Ballerina Salesforce connector docs
  • Ballerina Google Sheets connector docs

Summary by CodeRabbit

  • New Features

    • Enhanced sheet synchronization with improved normalization and intelligent default naming.
    • Added security validation for SOQL filters to prevent dangerous operations.
    • Improved timestamp precision by including seconds.
  • Bug Fixes

    • Better field mapping validation with comprehensive error messages.
    • More robust header management based on sheet state.
    • Case-insensitive field matching for improved reliability.

@coderabbitai

coderabbitai Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR refactors the lead synchronization workflow by introducing normalized state handling upfront, consolidating mode-specific branching through a new dispatcher function, implementing aggregated error reporting for field validation, and adding SOQL filter sanitization for security. The timestamp utility now includes seconds precision.

Changes

Cohort / File(s) Summary
Main Orchestration Logic
automation.bal
Added upfront normalization (trimmedSpreadsheetId, effectiveSyncMode, targetSheetName) and control-flow validation. Replaced direct branching to mode-specific functions with a new syncLeadsByMode() dispatcher. Updated appendLeads(), fullReplaceLeads(), and upsertLeadsByEmail() with normalized sheet names, improved header handling, and temp-sheet-based update mechanisms. Simplified group-key accumulation in split-sync logic. Added columnIndexToLetter() helper and replaced field-index lookups with case-insensitive getFieldIndex().
Field Mapping & Validation
data_mappings.bal
Replaced inline leadMap construction with dedicated LeadFieldMap record type and getLeadFieldMap() helper. Refactored mapLeadToRow() to use field comprehension and collect invalid fields into aggregated error message instead of failing on first invalid entry.
Utility Functions
functions.bal
Enhanced getFormattedCurrentTimeStamp() to include seconds in timestamp string. Added new sanitizeSoqlFilter() function that validates SOQL filters for dangerous DML keywords (delete, insert, update, merge, upsert, undelete) and returns error if found. Updated buildSoqlQuery() to sanitize filters before inclusion.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Leads hop through sheets with grace and care,
Validators catch what's foul in air,
A dispatcher routes them left and right,
Sanitized queries keep us safe and bright!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: resolving reviewer feedback on the Salesforce Leads to Google Sheets integration sample, which aligns with the comprehensive changes across automation.bal, data_mappings.bal, and functions.bal.
Description check ✅ Passed The description follows the required template with all essential sections completed including Purpose, Goals, Approach, User stories, Release notes, Security checks, Samples, and Test environment details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ballerina-integrator/salesforce-leads-to-googlesheets/automation.bal (1)

20-29: ⚠️ Potential issue | 🟡 Minor

Fail fast on UPSERT_BY_EMAIL + new spreadsheet before the empty-result return.

Line 20-23 exits before Line 27-29, so this invalid configuration is reported as success whenever the query returns 0 leads. The mode check should run before the no-leads early return, ideally before querying Salesforce.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ballerina-integrator/salesforce-leads-to-googlesheets/automation.bal` around
lines 20 - 29, The UPSERT_BY_EMAIL validity check is performed after the early
return on empty leadValues, causing invalid configs to be treated as success;
move the isNewSpreadsheet and effectiveSyncMode check (the branch that returns
error when UPSERT_BY_EMAIL is used with a new spreadsheet) so it runs before the
empty-result return — ideally before the Salesforce query is executed — ensuring
the validation (references: isNewSpreadsheet, effectiveSyncMode,
UPSERT_BY_EMAIL) always runs and returns the error for invalid configurations
regardless of lead count.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ballerina-integrator/salesforce-leads-to-googlesheets/automation.bal`:
- Around line 242-252: Wrap the sequence that clears the live sheet and copies
back from the temp sheet into a transactional block so the temp sheet is
preserved on failure: move the calls involving sheetsClient->clearRange(...,
sheet.properties.title), sheetsClient->getRange(..., tempSheet.properties.title)
/ appendValues(..., tempRange.values, {sheetName: sheet.properties.title}) and
sheetsClient->removeSheet(..., tempSheet.properties.sheetId) into a do ... on
fail block (or equivalent try/catch) so that if the append from temp to live
(the appendValues using tempRange.values) fails you do not remove the temp
sheet; instead keep the temp sheet and log/propagate the error. Ensure the clear
happens only just before a guaranteed successful copy-back (or roll back by
re-appending from temp in the fail handler), and reference tempSheetName,
tempSheet, tempRange, sheet.properties.title and sheetsClient methods above to
locate and modify the code.
- Around line 131-145: The code currently iterates all sheets and calls
sheetsClient->removeSheet, wiping the whole workbook; change the replace logic
to only target the sheet named effectiveSheetName (or its sheetId) so other tabs
remain. Specifically, after creating tempSheet (tempSheetName) and appending
values, locate the existing sheet in spreadsheet.sheets whose properties.title
== effectiveSheetName (or match by known owner set), call
sheetsClient->removeSheet(spreadsheetId, thatSheet.properties.sheetId) only for
that single sheet (skip if not found or if it's the same as tempSheet), then
call sheetsClient->renameSheet(spreadsheetId, tempSheet.properties.title,
effectiveSheetName); keep existing calls to applySheetFormatting and handle
errors as before.

In `@ballerina-integrator/salesforce-leads-to-googlesheets/data_mappings.bal`:
- Around line 55-69: The current validation of fieldMapping happens inside
mapLeadToRow (using getLeadFieldMap and leadMapAsMap), which runs per lead and
too late; move that validation into main before buildSoqlQuery and before
calling salesforceClient->query so typos/unsupported fields are detected up
front. In main, obtain the canonical set of supported field names (e.g., by
calling getLeadFieldMap on a prototype or a static definition used by
getLeadFieldMap), compare fieldMapping against that set, and if any
invalidFields exist return/log a single aggregated error and avoid calling
buildSoqlQuery or salesforceClient->query; remove or keep the per-lead check in
mapLeadToRow only as a defensive fallback.

In `@ballerina-integrator/salesforce-leads-to-googlesheets/functions.bal`:
- Around line 44-57: The current dangerousKeywords check uses substring matching
on lowerFilter (derived from trimmedFilter) and therefore rejects keywords
inside quoted literals like `'Merge Labs'`; change the logic to ignore/strip
single-quoted string literals from trimmedFilter first (e.g., remove or mask
content between single quotes), then perform a case-insensitive whole-token
match against dangerousKeywords (use word-boundary regex like /\b{keyword}\b/ or
split on non-alphanumeric chars and compare tokens) instead of using includes;
update the loop that iterates dangerousKeywords and the check that returns the
error so it references the filtered/unquoted string when testing for whole-token
matches.

---

Outside diff comments:
In `@ballerina-integrator/salesforce-leads-to-googlesheets/automation.bal`:
- Around line 20-29: The UPSERT_BY_EMAIL validity check is performed after the
early return on empty leadValues, causing invalid configs to be treated as
success; move the isNewSpreadsheet and effectiveSyncMode check (the branch that
returns error when UPSERT_BY_EMAIL is used with a new spreadsheet) so it runs
before the empty-result return — ideally before the Salesforce query is executed
— ensuring the validation (references: isNewSpreadsheet, effectiveSyncMode,
UPSERT_BY_EMAIL) always runs and returns the error for invalid configurations
regardless of lead count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d40d5ca-0982-44ca-b97e-5f6d414fb843

📥 Commits

Reviewing files that changed from the base of the PR and between 0fbb5b6 and 7e12dfd.

📒 Files selected for processing (3)
  • ballerina-integrator/salesforce-leads-to-googlesheets/automation.bal
  • ballerina-integrator/salesforce-leads-to-googlesheets/data_mappings.bal
  • ballerina-integrator/salesforce-leads-to-googlesheets/functions.bal

Comment on lines +131 to +145
string currentTimeStamp = check getFormattedCurrentTimeStamp();
string tempSheetName = string `${effectiveSheetName}_temp_${currentTimeStamp}`;
sheets:Sheet tempSheet = check sheetsClient->addSheet(spreadsheetId, tempSheetName);

_ = check sheetsClient->appendValues(spreadsheetId, allValues, {sheetName: newSheet.properties.title});
_ = check sheetsClient->appendValues(spreadsheetId, allValues, {sheetName: tempSheet.properties.title});

check applySheetFormatting(spreadsheetId, newSheet.properties.sheetId);
check applySheetFormatting(spreadsheetId, tempSheet.properties.sheetId);

_ = check sheetsClient->removeSheet(spreadsheetId, tempSheet.properties.sheetId);
do {
foreach sheets:Sheet sheet in spreadsheet.sheets {
_ = check sheetsClient->removeSheet(spreadsheetId, sheet.properties.sheetId);
}

_ = check sheetsClient->renameSheet(spreadsheetId, tempSheet.properties.title, effectiveSheetName);
} on fail error e {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

FULL_REPLACE is wiping the entire workbook.

Line 140-142 removes every sheet in the spreadsheet. That deletes unrelated tabs, and when syncLeadsSplit() invokes this once per group only the last group survives. The temp-sheet swap should replace just effectiveSheetName (or a known sample-owned set), not the whole workbook.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ballerina-integrator/salesforce-leads-to-googlesheets/automation.bal` around
lines 131 - 145, The code currently iterates all sheets and calls
sheetsClient->removeSheet, wiping the whole workbook; change the replace logic
to only target the sheet named effectiveSheetName (or its sheetId) so other tabs
remain. Specifically, after creating tempSheet (tempSheetName) and appending
values, locate the existing sheet in spreadsheet.sheets whose properties.title
== effectiveSheetName (or match by known owner set), call
sheetsClient->removeSheet(spreadsheetId, thatSheet.properties.sheetId) only for
that single sheet (skip if not found or if it's the same as tempSheet), then
call sheetsClient->renameSheet(spreadsheetId, tempSheet.properties.title,
effectiveSheetName); keep existing calls to applySheetFormatting and handle
errors as before.

Comment on lines +242 to +252
string tempSheetName = string `${sheet.properties.title}_temp_${check getFormattedCurrentTimeStamp()}`;
sheets:Sheet tempSheet = check sheetsClient->addSheet(spreadsheetId, tempSheetName);

_ = check sheetsClient->appendValues(spreadsheetId, allData, {sheetName: tempSheet.properties.title});

_ = check sheetsClient->clearRange(spreadsheetId, sheet.properties.title, a1Notation = string `A:${endColumn}`);
_ = check sheetsClient->appendValues(spreadsheetId, allData, {sheetName: sheet.properties.title});

check applySheetFormatting(spreadsheetId, sheet.properties.sheetId);
sheets:Range tempRange = check sheetsClient->getRange(spreadsheetId, tempSheet.properties.title, a1Notation = string `A:${endColumn}`);
_ = check sheetsClient->appendValues(spreadsheetId, tempRange.values, {sheetName: sheet.properties.title});

if newLeads.length() > 0 {
_ = check sheetsClient->appendValues(spreadsheetId, newLeads, {sheetName: sheet.properties.title});
}
_ = check sheetsClient->removeSheet(spreadsheetId, tempSheet.properties.sheetId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the temp-sheet copy-back in UPSERT transactional.

After Line 247 clears the live sheet, any failure in Line 249-252 leaves the original tab blank or partially restored. Wrap the clear/copy/remove sequence in do/on fail and keep the temp sheet until the copy-back succeeds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ballerina-integrator/salesforce-leads-to-googlesheets/automation.bal` around
lines 242 - 252, Wrap the sequence that clears the live sheet and copies back
from the temp sheet into a transactional block so the temp sheet is preserved on
failure: move the calls involving sheetsClient->clearRange(...,
sheet.properties.title), sheetsClient->getRange(..., tempSheet.properties.title)
/ appendValues(..., tempRange.values, {sheetName: sheet.properties.title}) and
sheetsClient->removeSheet(..., tempSheet.properties.sheetId) into a do ... on
fail block (or equivalent try/catch) so that if the append from temp to live
(the appendValues using tempRange.values) fails you do not remove the temp
sheet; instead keep the temp sheet and log/propagate the error. Ensure the clear
happens only just before a guaranteed successful copy-back (or roll back by
re-appending from temp in the fail handler), and reference tempSheetName,
tempSheet, tempRange, sheet.properties.title and sheetsClient methods above to
locate and modify the code.

Comment on lines +55 to +69
public function mapLeadToRow(Lead lead) returns SheetRow|error {
LeadFieldMap leadMap = getLeadFieldMap(lead);
map<int|string|decimal|boolean|float> leadMapAsMap = leadMap;

SheetRow row = from string fieldName in fieldMapping
select leadMapAsMap.hasKey(fieldName) ?
leadMapAsMap.get(fieldName) :
"";

string[] invalidFields = from string fieldName in fieldMapping
where !leadMapAsMap.hasKey(fieldName)
select fieldName;

if invalidFields.length() > 0 {
return error(string `Invalid field name(s) in fieldMapping: ${string:'join(", ", ...invalidFields)}. Supported fields are: ${string:'join(", ", ...leadMapAsMap.keys())}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate fieldMapping before the Salesforce query, not per lead.

main() builds and executes the SOQL from fieldMapping before mapLeadToRow() ever runs. That means typos still fail at salesforceClient->query(...), and valid-but-unsupported fields are never reported when the stream is empty. Move this validation ahead of buildSoqlQuery() so the aggregated error is actually reachable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ballerina-integrator/salesforce-leads-to-googlesheets/data_mappings.bal`
around lines 55 - 69, The current validation of fieldMapping happens inside
mapLeadToRow (using getLeadFieldMap and leadMapAsMap), which runs per lead and
too late; move that validation into main before buildSoqlQuery and before
calling salesforceClient->query so typos/unsupported fields are detected up
front. In main, obtain the canonical set of supported field names (e.g., by
calling getLeadFieldMap on a prototype or a static definition used by
getLeadFieldMap), compare fieldMapping against that set, and if any
invalidFields exist return/log a single aggregated error and avoid calling
buildSoqlQuery or salesforceClient->query; remove or keep the per-lead check in
mapLeadToRow only as a defensive fallback.

Comment on lines +44 to +57
string lowerFilter = trimmedFilter.toLowerAscii();

string[] dangerousKeywords = [
"delete",
"insert",
"update",
"merge",
"upsert",
"undelete"
];

foreach string keyword in dangerousKeywords {
if lowerFilter.includes(keyword) {
return error(string `SOQL filter contains dangerous keyword: "${keyword}". Only SELECT queries are allowed.`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid substring blacklisting across the whole filter.

Line 56 also scans quoted literals, so valid predicates like Company = 'Merge Labs' or Status = 'Updated' are rejected because they contain merge/update. Match whole tokens outside string values instead; otherwise legitimate soqlFilter values stop working.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ballerina-integrator/salesforce-leads-to-googlesheets/functions.bal` around
lines 44 - 57, The current dangerousKeywords check uses substring matching on
lowerFilter (derived from trimmedFilter) and therefore rejects keywords inside
quoted literals like `'Merge Labs'`; change the logic to ignore/strip
single-quoted string literals from trimmedFilter first (e.g., remove or mask
content between single quotes), then perform a case-insensitive whole-token
match against dangerousKeywords (use word-boundary regex like /\b{keyword}\b/ or
split on non-alphanumeric chars and compare tokens) instead of using includes;
update the loop that iterates dangerousKeywords and the check that returns the
error so it references the filtered/unquoted string when testing for whole-token
matches.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant