From 83ae4e974d6bae52d416c7f62a49edda9dbf7342 Mon Sep 17 00:00:00 2001 From: TMTrevisan Date: Tue, 3 Mar 2026 15:59:24 -0800 Subject: [PATCH] feat: Add native Windows PowerShell rollout scripts and overhaul README Resolves compatibility issues for Windows users unable to execute bash scripts smoothly. Overhaul README to provide clear instructions for multiple operating systems. --- README.md | 85 +++++++-- Scripts/ps1/activate_trigger_handlers.ps1 | 213 ++++++++++++++++++++++ Scripts/ps1/data_load.ps1 | 31 ++++ 3 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 Scripts/ps1/activate_trigger_handlers.ps1 create mode 100644 Scripts/ps1/data_load.ps1 diff --git a/README.md b/README.md index 6564c7d..582ff0e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,73 @@ -# README - -# Prerequisites -1. LSC4CE Org with 2GP package installed is available -2. Org must have the permission set licenses "Health Cloud Starter" and "Life Science Commercial" - -# Steps to load data into Org -1. Clone the repo “https://github.com/SalesforceLabs/LSStarterConfig.git” to local on a laptop -2. Open the SFDX project “LSStarterConfig” in Visual Studio Code -3. Authorize the LSC4CE Org and connect to the Org. -4. Open Terminal in Visual Studio Code and run the command “npm install” from "LSStarterConfig" folder -5. From Terminal in Visual Studio Code run the command "sh Scripts/sh/data_load.sh" from "LSStarterConfig" folder +# LS Starter Config + +This repository contains the starter configuration records, profiles, and trigger handlers necessary to initialize a Life Sciences Cloud org or sandbox. + +## Prerequisites + +Before deploying this package to your org, ensure your environment is set up correctly: + +1. **Target Org Requirements:** + * A Life Sciences Cloud (LSC4CE) Org with the 2GP package installed. + * The org MUST have the following permission set licenses available: + * `Health Cloud Starter` + * `Life Science Commercial` +2. **Local Environment Requirements:** + * [Salesforce CLI (`sf`)](https://developer.salesforce.com/tools/salesforcecli) installed. + * [Node.js and npm](https://nodejs.org/en) installed. + * *(Mac/Linux only)* `jq` installed (`brew install jq` or `apt-get install jq`). + +--- + +## Deployment Steps + +Choose the appropriate tab below depending on your operating system (Mac/Linux vs. Windows). + +### 1. Authorize Your Org +First, authenticate the Salesforce CLI with the exact org where you wish to deploy these configurations. + +**For Production:** +```bash +sf org login web --set-default +``` + +**For Sandboxes:** +```bash +sf org login web -r https://test.salesforce.com --set-default +``` +*(Follow the browser prompts to log in and allow CLI access).* + +### 2. Install Dependencies +Install the local tooling required to deploy the project (like Prettier and testing frameworks): +```bash +npm install +``` + +### 3. Run the Data Loader Script +This script will deploy the custom profile, seed metadata framework records, push configuration records, and activate all necessary trigger handlers. + +**Mac / Linux:** +```bash +sh Scripts/sh/data_load.sh +``` + +**Windows (PowerShell):** +```powershell +.\Scripts\ps1\data_load.ps1 +``` + +--- + +## Troubleshooting + +### Error: `Unknown user permission: EnableCommunityAppLauncher` +If your deployment fails loudly on the `LSC Custom Profile` step stating that `EnableCommunityAppLauncher` is an unknown user permission, this means your specific org shape does not support this permission. + +**Fix:** +1. Open `PackageComponents/profiles/LSC Custom Profile.profile-meta.xml`. +2. Locate the `` block containing `EnableCommunityAppLauncher` (around line 93). +3. Delete that entire block. +4. Re-run the data loader script. + +### Warnings: `DUPLICATE_VALUE` on tree import +During step 3 (Data Loader), you may see a table of errors stating `duplicate value found: Name duplicates value on record with id...`. +This is **safe to ignore**. It simply means that your org already contains the standard Life Sciences metadata categories provided by the package, so the CLI skipped recreating them. diff --git a/Scripts/ps1/activate_trigger_handlers.ps1 b/Scripts/ps1/activate_trigger_handlers.ps1 new file mode 100644 index 0000000..b9753a4 --- /dev/null +++ b/Scripts/ps1/activate_trigger_handlers.ps1 @@ -0,0 +1,213 @@ +<# +.SYNOPSIS +Activates/Deactivates LifeScienceTriggerHandler records by DeveloperName. + +.DESCRIPTION +- Uses the Tooling API for setup entity updates (no REST calls). +- Accepts names via -Names or -File. If none provided, extracts DeveloperName values + from TriggerHandlers.ts in the current directory. + +# Requirements: Salesforce CLI (sf or sfdx) + +.EXAMPLE +.\activate_trigger_handlers.ps1 -org myAlias -names "HandlerA,HandlerB" +.\activate_trigger_handlers.ps1 -org myAlias -file handlers.txt +.\activate_trigger_handlers.ps1 -org myAlias +#> + +param ( + [string]$org = "", + [string]$names = "", + [string]$file = "", + [string]$apiVersion = "65.0", + [switch]$deactivate, + [switch]$verboseOut +) + +$ErrorActionPreference = "Stop" + +# Use sf if available, fallback to sfdx +$sfCli = "sf" +if (-not (Get-Command $sfCli -ErrorAction SilentlyContinue)) { + $sfCli = "sfdx" + if (-not (Get-Command $sfCli -ErrorAction SilentlyContinue)) { + Write-Error "Salesforce CLI (sf or sfdx) not found" + exit 1 + } +} + +# --- 1. Get Authentication Info --- +function Get-OrgAuth { + param([string]$orgAlias) + + $authJson = "" + if ($sfCli -eq "sf") { + if ([string]::IsNullOrWhiteSpace($orgAlias)) { + $authJson = sf org display --json + } else { + $authJson = sf org display --json --target-org "$orgAlias" + } + } else { + if ([string]::IsNullOrWhiteSpace($orgAlias)) { + $authJson = sfdx force:org:display --json + } else { + $authJson = sfdx force:org:display --json -u "$orgAlias" + } + } + + return $authJson | ConvertFrom-Json +} + +Write-Host "Retrieving Org Authentication..." +$authObj = Get-OrgAuth -orgAlias $org + +$accessToken = $authObj.result.accessToken +$instanceUrl = $authObj.result.instanceUrl + +if ([string]::IsNullOrEmpty($accessToken) -or [string]::IsNullOrEmpty($instanceUrl)) { + Write-Error "Could not retrieve access token or instance URL. Ensure you are logged into a default org or provide -org." + exit 1 +} + +if ($verboseOut) { + Write-Host "Instance URL: $instanceUrl" + Write-Host "API Version: $apiVersion" +} + + +# --- 2. Determine Developer Names to Process --- +$developerNames = @() + +if (-not [string]::IsNullOrWhiteSpace($names)) { + $developerNames = $names -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } +} +elseif (-not [string]::IsNullOrWhiteSpace($file)) { + if (-not (Test-Path $file)) { + Write-Error "File not found: $file" + exit 1 + } + $developerNames = Get-Content $file | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } +} +else { + # Default behavior: parse TriggerHandlers.ts + Write-Host "No -names or -file provided. Parsing DeveloperName from TriggerHandlers.ts..." + $tsPath = "TriggerHandlers.ts" + if (-not (Test-Path $tsPath)) { + # Assume it might be executed from the root, try that path. + $tsPath = "TriggerHandlers\TriggerHandlers.ts" + } + + if (Test-Path $tsPath) { + $content = Get-Content $tsPath + # Regex to match DeveloperName: "value" or 'DeveloperName': "value" + foreach ($line in $content) { + if ($line -match "['""]?DeveloperName['""]?\s*:\s*""([^""]+)""") { + $developerNames += $matches[1] + } + } + $developerNames = $developerNames | Select-Object -Unique + } else { + Write-Error "TriggerHandlers.ts not found. Please specify -names or -file." + exit 1 + } +} + +if ($developerNames.Count -eq 0) { + Write-Error "No DeveloperName values found." + exit 1 +} + +# --- 3. Processing Handlers --- +$headers = @{ + "Authorization" = "Bearer $accessToken" + "Content-Type" = "application/json" +} + +$successCount = 0 +$skippedCount = 0 +$notFoundCount = 0 +$failedCount = 0 + +foreach ($devName in $developerNames) { + if ($verboseOut) { Write-Host "Processing DeveloperName: $devName" } + + $soql = "SELECT Id, IsActive, DeveloperName FROM LifeScienceTriggerHandler WHERE DeveloperName = '$devName'" + $encodedSoql = [uri]::EscapeDataString($soql) + + $record = $null + + # Tooling API Query + $toolingUrl = "$instanceUrl/services/data/v$apiVersion/tooling/query?q=$encodedSoql" + try { + $toolingResponse = Invoke-RestMethod -Uri $toolingUrl -Headers $headers -Method Get + if ($toolingResponse.totalSize -gt 0) { + $record = $toolingResponse.records[0] + } + } catch { + # Fallback to standard REST API Query + $restUrl = "$instanceUrl/services/data/v$apiVersion/query?q=$encodedSoql" + try { + $restResponse = Invoke-RestMethod -Uri $restUrl -Headers $headers -Method Get + if ($restResponse.totalSize -gt 0) { + $record = $restResponse.records[0] + } + } catch { } + } + + if ($null -eq $record) { + if ($verboseOut) { Write-Host " Not found in org." } + $notFoundCount++ + continue + } + + $id = $record.Id + $isActive = $record.IsActive + $targetActive = -not $deactivate + + if ($targetActive -eq $isActive) { + if ($verboseOut) { Write-Host " Already in desired state. Skipping." } + $skippedCount++ + continue + } + + $bodyMap = @{ "IsActive" = $targetActive } + $bodyJson = $bodyMap | ConvertTo-Json -Compress + + $updateSuccess = $false + + # Attempt Standard sObject REST Update + $updateStdUrl = "$instanceUrl/services/data/v$apiVersion/sobjects/LifeScienceTriggerHandler/$id" + try { + $updateResp = Invoke-RestMethod -Uri $updateStdUrl -Headers $headers -Method Patch -Body $bodyJson + $updateSuccess = $true + if ($verboseOut) { Write-Host " Updated via Standard REST." } + } catch { + # Attempt Tooling API Update + $updateToolUrl = "$instanceUrl/services/data/v$apiVersion/tooling/sobjects/LifeScienceTriggerHandler/$id" + try { + $updateResp = Invoke-RestMethod -Uri $updateToolUrl -Headers $headers -Method Patch -Body $bodyJson + $updateSuccess = $true + if ($verboseOut) { Write-Host " Updated via Tooling API." } + } catch { } + } + + if ($updateSuccess) { + $successCount++ + } else { + if ($verboseOut) { Write-Host " Update failed." } + $failedCount++ + } +} + +Write-Host "`nDone. Summary:" +$successLabel = if ($deactivate) { "Deactivated" } else { "Activated" } +$skippedLabel = if ($deactivate) { "Already Inactive" } else { "Already Active" } +Write-Host " $successLabel : $successCount" +Write-Host " $skippedLabel : $skippedCount" +Write-Host " Not Found : $notFoundCount" +Write-Host " Failed : $failedCount" + +if ($failedCount -gt 0) { + exit 1 +} +exit 0 diff --git a/Scripts/ps1/data_load.ps1 b/Scripts/ps1/data_load.ps1 new file mode 100644 index 0000000..57e34a1 --- /dev/null +++ b/Scripts/ps1/data_load.ps1 @@ -0,0 +1,31 @@ +# data_load.ps1 +# Deploys LSC Starter Configurations +$ErrorActionPreference = "Stop" + +Write-Host "Deploying configurations..." + +# Verify Salesforce CLI is installed +if (-not (Get-Command "sf" -ErrorAction SilentlyContinue) -and -not (Get-Command "sfdx" -ErrorAction SilentlyContinue)) { + Write-Error "Salesforce CLI (sf or sfdx) could not be found. Please install the Salesforce CLI to proceed." + exit 1 +} + +# 1. Deploy the LSC Custom Profile +Write-Host "Deploying LSC Custom Profile..." +sf project deploy start -d "PackageComponents/profiles/LSC Custom Profile.profile-meta.xml" --json *> $null + +# 2. Import Metadata Categories +Write-Host "Importing LifeSciMetadataCategories..." +# Suppress output; duplicates will fail silently as intended by the original script +sf data import tree --plan LSConfig/lifeSciMetadataRecord/LifeSciMetadataCategory-plan.json --json *> $null + +# 3. Deploy configuration records +Write-Host "Deploying Config Records..." +sf project deploy start -d LSConfig/lifeSciConfigRecord --json *> $null + +# 4. Activate Trigger Handlers +Write-Host "Activating Trigger Handlers..." +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +& "$scriptDir\activate_trigger_handlers.ps1" -file "TriggerHandlers\TriggerHandlers.ts" + +Write-Host "Deployment Completed Successfuly!"