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!"