Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 73 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 `<userPermissions>` block containing `<name>EnableCommunityAppLauncher</name>` (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.
213 changes: 213 additions & 0 deletions Scripts/ps1/activate_trigger_handlers.ps1
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions Scripts/ps1/data_load.ps1
Original file line number Diff line number Diff line change
@@ -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!"