scc-universal 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +44 -0
- package/.cursor/agents/deep-researcher.md +142 -0
- package/.cursor/agents/doc-updater.md +219 -0
- package/.cursor/agents/eval-runner.md +335 -0
- package/.cursor/agents/learning-engine.md +210 -0
- package/.cursor/agents/loop-operator.md +245 -0
- package/.cursor/agents/refactor-cleaner.md +119 -0
- package/.cursor/agents/sf-admin-agent.md +127 -0
- package/.cursor/agents/sf-agentforce-agent.md +126 -0
- package/.cursor/agents/sf-apex-agent.md +117 -0
- package/.cursor/agents/sf-architect.md +426 -0
- package/.cursor/agents/sf-aura-reviewer.md +369 -0
- package/.cursor/agents/sf-bugfix-agent.md +101 -0
- package/.cursor/agents/sf-flow-agent.md +155 -0
- package/.cursor/agents/sf-integration-agent.md +141 -0
- package/.cursor/agents/sf-lwc-agent.md +123 -0
- package/.cursor/agents/sf-review-agent.md +357 -0
- package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
- package/.cursor/hooks/adapter.js +81 -0
- package/.cursor/hooks/after-file-edit.js +26 -0
- package/.cursor/hooks/after-mcp-execution.js +12 -0
- package/.cursor/hooks/after-shell-execution.js +30 -0
- package/.cursor/hooks/after-tab-file-edit.js +12 -0
- package/.cursor/hooks/before-mcp-execution.js +11 -0
- package/.cursor/hooks/before-read-file.js +13 -0
- package/.cursor/hooks/before-shell-execution.js +29 -0
- package/.cursor/hooks/before-submit-prompt.js +23 -0
- package/.cursor/hooks/pre-compact.js +7 -0
- package/.cursor/hooks/session-end.js +10 -0
- package/.cursor/hooks/session-start.js +10 -0
- package/.cursor/hooks/stop.js +18 -0
- package/.cursor/hooks/subagent-start.js +10 -0
- package/.cursor/hooks/subagent-stop.js +10 -0
- package/.cursor/hooks.json +107 -0
- package/.cursor/skills/aside/SKILL.md +115 -0
- package/.cursor/skills/checkpoint/SKILL.md +50 -0
- package/.cursor/skills/configure-scc/SKILL.md +160 -0
- package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
- package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
- package/.cursor/skills/model-route/SKILL.md +81 -0
- package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
- package/.cursor/skills/refactor-clean/SKILL.md +133 -0
- package/.cursor/skills/resume-session/SKILL.md +111 -0
- package/.cursor/skills/save-session/SKILL.md +183 -0
- package/.cursor/skills/search-first/SKILL.md +140 -0
- package/.cursor/skills/security-scan/SKILL.md +142 -0
- package/.cursor/skills/sessions/SKILL.md +124 -0
- package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
- package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
- package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
- package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
- package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
- package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
- package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
- package/.cursor/skills/sf-api-design/SKILL.md +237 -0
- package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
- package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
- package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
- package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
- package/.cursor/skills/sf-debugging/SKILL.md +362 -0
- package/.cursor/skills/sf-deployment/SKILL.md +291 -0
- package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
- package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
- package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
- package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
- package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
- package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
- package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
- package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
- package/.cursor/skills/sf-help/SKILL.md +156 -0
- package/.cursor/skills/sf-integration/SKILL.md +479 -0
- package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
- package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
- package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
- package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
- package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
- package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
- package/.cursor/skills/sf-security/SKILL.md +330 -0
- package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
- package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
- package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
- package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
- package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
- package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
- package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
- package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
- package/.cursor/skills/strategic-compact/SKILL.md +205 -0
- package/.cursor/skills/update-docs/SKILL.md +162 -0
- package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/agents/deep-researcher.md +145 -0
- package/agents/doc-updater.md +222 -0
- package/agents/eval-runner.md +340 -0
- package/agents/learning-engine.md +211 -0
- package/agents/loop-operator.md +247 -0
- package/agents/refactor-cleaner.md +122 -0
- package/agents/sf-admin-agent.md +131 -0
- package/agents/sf-agentforce-agent.md +132 -0
- package/agents/sf-apex-agent.md +124 -0
- package/agents/sf-architect.md +435 -0
- package/agents/sf-aura-reviewer.md +372 -0
- package/agents/sf-bugfix-agent.md +105 -0
- package/agents/sf-flow-agent.md +159 -0
- package/agents/sf-integration-agent.md +146 -0
- package/agents/sf-lwc-agent.md +127 -0
- package/agents/sf-review-agent.md +366 -0
- package/agents/sf-visualforce-reviewer.md +468 -0
- package/assets/logo.svg +18 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/authoring-guide.md +373 -0
- package/docs/hook-development.md +578 -0
- package/docs/token-optimization.md +139 -0
- package/docs/workflow-examples.md +645 -0
- package/examples/agentforce-action/README.md +227 -0
- package/examples/apex-trigger-handler/README.md +114 -0
- package/examples/devops-pipeline/README.md +325 -0
- package/examples/flow-automation/README.md +188 -0
- package/examples/integration-pattern/README.md +416 -0
- package/examples/lwc-component/README.md +180 -0
- package/examples/platform-events/README.md +492 -0
- package/examples/scratch-org-setup/README.md +138 -0
- package/examples/security-audit/README.md +244 -0
- package/examples/visualforce-migration/README.md +314 -0
- package/hooks/hooks.json +338 -0
- package/hooks/memory-persistence/README.md +73 -0
- package/manifests/install-modules.json +217 -0
- package/manifests/install-profiles.json +17 -0
- package/mcp-configs/mcp-servers.json +19 -0
- package/package.json +89 -0
- package/schemas/hooks.schema.json +123 -0
- package/schemas/install-modules.schema.json +76 -0
- package/schemas/install-profiles.schema.json +28 -0
- package/schemas/install-state.schema.json +73 -0
- package/schemas/package-manager.schema.json +18 -0
- package/schemas/plugin.schema.json +112 -0
- package/schemas/scc-install-config.schema.json +29 -0
- package/schemas/state-store.schema.json +111 -0
- package/scripts/cli/install-apply.js +170 -0
- package/scripts/cli/uninstall.js +193 -0
- package/scripts/hooks/check-console-log.js +101 -0
- package/scripts/hooks/check-hook-enabled.js +17 -0
- package/scripts/hooks/check-platform-docs-age.js +48 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +98 -0
- package/scripts/hooks/governor-check.js +220 -0
- package/scripts/hooks/learning-observe.sh +206 -0
- package/scripts/hooks/mcp-health-check.js +588 -0
- package/scripts/hooks/post-bash-build-complete.js +34 -0
- package/scripts/hooks/post-bash-pr-created.js +43 -0
- package/scripts/hooks/post-edit-console-warn.js +61 -0
- package/scripts/hooks/post-edit-format.js +79 -0
- package/scripts/hooks/post-edit-typecheck.js +98 -0
- package/scripts/hooks/post-write.js +168 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
- package/scripts/hooks/pre-compact.js +51 -0
- package/scripts/hooks/pre-tool-use.js +163 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +251 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +135 -0
- package/scripts/hooks/session-end-marker.js +29 -0
- package/scripts/hooks/session-end.js +311 -0
- package/scripts/hooks/session-start.js +202 -0
- package/scripts/hooks/sfdx-scanner-check.js +142 -0
- package/scripts/hooks/sfdx-validate.js +119 -0
- package/scripts/hooks/stop-hook.js +170 -0
- package/scripts/hooks/suggest-compact.js +67 -0
- package/scripts/lib/agent-adapter.js +82 -0
- package/scripts/lib/apex-analysis.js +194 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install-config.js +73 -0
- package/scripts/lib/install-executor.js +363 -0
- package/scripts/lib/install-state.js +121 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.js +124 -0
- package/scripts/lib/project-detect.js +228 -0
- package/scripts/lib/schema-validator.js +190 -0
- package/scripts/lib/skill-adapter.js +100 -0
- package/scripts/lib/state-store.js +376 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
- package/scripts/lib/utils.js +313 -0
- package/scripts/scc.js +164 -0
- package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
- package/skills/_reference/APEX_CURSOR.md +159 -0
- package/skills/_reference/API_VERSIONS.md +78 -0
- package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
- package/skills/_reference/ASYNC_PATTERNS.md +163 -0
- package/skills/_reference/AURA_COMPONENTS.md +146 -0
- package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
- package/skills/_reference/DATA_MODELING.md +124 -0
- package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
- package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
- package/skills/_reference/DEPRECATIONS.md +79 -0
- package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
- package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
- package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
- package/skills/_reference/FLOW_PATTERNS.md +113 -0
- package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
- package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
- package/skills/_reference/LWC_PATTERNS.md +79 -0
- package/skills/_reference/METADATA_TYPES.md +115 -0
- package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
- package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
- package/skills/_reference/PLATFORM_EVENTS.md +121 -0
- package/skills/_reference/REPORTING_API.md +143 -0
- package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
- package/skills/_reference/SECURITY_PATTERNS.md +127 -0
- package/skills/_reference/SHARING_MODEL.md +120 -0
- package/skills/_reference/SOQL_PATTERNS.md +119 -0
- package/skills/_reference/TESTING_STANDARDS.md +96 -0
- package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
- package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
- package/skills/aside/SKILL.md +118 -0
- package/skills/checkpoint/SKILL.md +53 -0
- package/skills/configure-scc/SKILL.md +163 -0
- package/skills/continuous-agent-loop/SKILL.md +264 -0
- package/skills/mcp-server-patterns/SKILL.md +146 -0
- package/skills/model-route/SKILL.md +84 -0
- package/skills/prompt-optimizer/SKILL.md +369 -0
- package/skills/refactor-clean/SKILL.md +136 -0
- package/skills/resume-session/SKILL.md +114 -0
- package/skills/save-session/SKILL.md +186 -0
- package/skills/search-first/SKILL.md +144 -0
- package/skills/security-scan/SKILL.md +146 -0
- package/skills/sessions/SKILL.md +127 -0
- package/skills/sf-agentforce-development/SKILL.md +450 -0
- package/skills/sf-apex-async-patterns/SKILL.md +326 -0
- package/skills/sf-apex-best-practices/SKILL.md +425 -0
- package/skills/sf-apex-constraints/SKILL.md +81 -0
- package/skills/sf-apex-cursor/SKILL.md +338 -0
- package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
- package/skills/sf-apex-testing/SKILL.md +409 -0
- package/skills/sf-api-design/SKILL.md +238 -0
- package/skills/sf-approval-processes/SKILL.md +315 -0
- package/skills/sf-aura-development/SKILL.md +263 -0
- package/skills/sf-build-fix/SKILL.md +121 -0
- package/skills/sf-data-modeling/SKILL.md +278 -0
- package/skills/sf-debugging/SKILL.md +363 -0
- package/skills/sf-deployment/SKILL.md +295 -0
- package/skills/sf-deployment-constraints/SKILL.md +155 -0
- package/skills/sf-devops-ci-cd/SKILL.md +325 -0
- package/skills/sf-docs-lookup/SKILL.md +103 -0
- package/skills/sf-e2e-testing/SKILL.md +324 -0
- package/skills/sf-experience-cloud/SKILL.md +249 -0
- package/skills/sf-flow-development/SKILL.md +377 -0
- package/skills/sf-governor-limits/SKILL.md +323 -0
- package/skills/sf-harness-audit/SKILL.md +142 -0
- package/skills/sf-help/SKILL.md +159 -0
- package/skills/sf-integration/SKILL.md +483 -0
- package/skills/sf-lwc-constraints/SKILL.md +130 -0
- package/skills/sf-lwc-development/SKILL.md +303 -0
- package/skills/sf-lwc-testing/SKILL.md +388 -0
- package/skills/sf-metadata-management/SKILL.md +288 -0
- package/skills/sf-platform-events-cdc/SKILL.md +375 -0
- package/skills/sf-quickstart/SKILL.md +173 -0
- package/skills/sf-security/SKILL.md +334 -0
- package/skills/sf-security-constraints/SKILL.md +127 -0
- package/skills/sf-soql-constraints/SKILL.md +131 -0
- package/skills/sf-soql-optimization/SKILL.md +354 -0
- package/skills/sf-tdd-workflow/SKILL.md +336 -0
- package/skills/sf-testing-constraints/SKILL.md +200 -0
- package/skills/sf-trigger-constraints/SKILL.md +90 -0
- package/skills/sf-trigger-frameworks/SKILL.md +347 -0
- package/skills/sf-visualforce-development/SKILL.md +260 -0
- package/skills/strategic-compact/SKILL.md +208 -0
- package/skills/update-docs/SKILL.md +165 -0
- package/skills/update-platform-docs/SKILL.md +90 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Scratch Org Setup Example
|
|
2
|
+
|
|
3
|
+
Complete scratch org configuration with definition file, setup scripts, and sample data.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
config/
|
|
9
|
+
project-scratch-def.json # Scratch org definition
|
|
10
|
+
scripts/
|
|
11
|
+
setup-scratch-org.sh # Automated setup script
|
|
12
|
+
data/
|
|
13
|
+
sample-accounts.json # Sample data for development
|
|
14
|
+
sfdx-project.json # Project configuration
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Scratch Org Definition
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"orgName": "SCC Dev Scratch Org",
|
|
22
|
+
"edition": "Developer",
|
|
23
|
+
"features": [
|
|
24
|
+
"EnableSetPasswordInApi",
|
|
25
|
+
"Communities",
|
|
26
|
+
"ServiceCloud",
|
|
27
|
+
"LightningSalesConsole"
|
|
28
|
+
],
|
|
29
|
+
"settings": {
|
|
30
|
+
"lightningExperienceSettings": {
|
|
31
|
+
"enableS1DesktopEnabled": true
|
|
32
|
+
},
|
|
33
|
+
"securitySettings": {
|
|
34
|
+
"passwordPolicies": {
|
|
35
|
+
"enableSetPasswordInApi": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"mobileSettings": {
|
|
39
|
+
"enableS1EncryptedStoragePref2": false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Project Configuration (sfdx-project.json)
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"packageDirectories": [
|
|
50
|
+
{
|
|
51
|
+
"path": "force-app",
|
|
52
|
+
"default": true
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"namespace": "",
|
|
56
|
+
"sfdcLoginUrl": "https://login.salesforce.com",
|
|
57
|
+
"sourceApiVersion": "66.0"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Setup Script
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
#!/bin/bash
|
|
65
|
+
set -e
|
|
66
|
+
|
|
67
|
+
ORG_ALIAS=${1:-dev-scratch}
|
|
68
|
+
DURATION=${2:-7}
|
|
69
|
+
|
|
70
|
+
echo "Creating scratch org: $ORG_ALIAS (${DURATION} days)"
|
|
71
|
+
sf org create scratch \
|
|
72
|
+
--definition-file config/project-scratch-def.json \
|
|
73
|
+
--alias "$ORG_ALIAS" \
|
|
74
|
+
--duration-days "$DURATION" \
|
|
75
|
+
--set-default \
|
|
76
|
+
--wait 15
|
|
77
|
+
|
|
78
|
+
echo "Deploying source..."
|
|
79
|
+
sf project deploy start --target-org "$ORG_ALIAS"
|
|
80
|
+
|
|
81
|
+
echo "Assigning permission sets..."
|
|
82
|
+
sf org assign permset --name SCC_Admin --target-org "$ORG_ALIAS" || true
|
|
83
|
+
|
|
84
|
+
echo "Importing sample data..."
|
|
85
|
+
sf data import tree --files data/sample-accounts.json --target-org "$ORG_ALIAS" || true
|
|
86
|
+
|
|
87
|
+
echo "Running tests..."
|
|
88
|
+
sf apex run test --test-level RunLocalTests --target-org "$ORG_ALIAS" --result-format human
|
|
89
|
+
|
|
90
|
+
echo "Opening org..."
|
|
91
|
+
sf org open --target-org "$ORG_ALIAS"
|
|
92
|
+
|
|
93
|
+
echo "Scratch org $ORG_ALIAS is ready!"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Sample Data
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"records": [
|
|
101
|
+
{
|
|
102
|
+
"attributes": { "type": "Account", "referenceId": "AccRef1" },
|
|
103
|
+
"Name": "Acme Corporation",
|
|
104
|
+
"Industry": "Technology",
|
|
105
|
+
"AnnualRevenue": 5000000,
|
|
106
|
+
"NumberOfEmployees": 250
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"attributes": { "type": "Account", "referenceId": "AccRef2" },
|
|
110
|
+
"Name": "Global Industries",
|
|
111
|
+
"Industry": "Manufacturing",
|
|
112
|
+
"AnnualRevenue": 12000000,
|
|
113
|
+
"NumberOfEmployees": 1500
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Lifecycle Commands
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Create
|
|
123
|
+
sf org create scratch -f config/project-scratch-def.json -a my-scratch -d 7
|
|
124
|
+
|
|
125
|
+
# Develop
|
|
126
|
+
sf project deploy start # Push source
|
|
127
|
+
sf project retrieve start # Pull changes from org
|
|
128
|
+
|
|
129
|
+
# Test
|
|
130
|
+
sf apex run test --test-level RunLocalTests
|
|
131
|
+
|
|
132
|
+
# Inspect
|
|
133
|
+
sf org display # Show org details
|
|
134
|
+
sf org open # Open in browser
|
|
135
|
+
|
|
136
|
+
# Cleanup
|
|
137
|
+
sf org delete scratch -o my-scratch --no-prompt
|
|
138
|
+
```
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Security Audit Walkthrough
|
|
2
|
+
|
|
3
|
+
Step-by-step security audit for Salesforce Apex code covering CRUD/FLS, SOQL injection, sharing model, and static analysis. Patterns verified for API version 66.0 (Spring '26).
|
|
4
|
+
|
|
5
|
+
## When to Use This Pattern
|
|
6
|
+
|
|
7
|
+
- Before deploying Apex code to production
|
|
8
|
+
- During security reviews or AppExchange security submissions
|
|
9
|
+
- When refactoring legacy code that bypasses security controls
|
|
10
|
+
- After a Salesforce security health check flags issues
|
|
11
|
+
|
|
12
|
+
## CRUD/FLS Enforcement
|
|
13
|
+
|
|
14
|
+
### Before (Insecure)
|
|
15
|
+
|
|
16
|
+
```apex
|
|
17
|
+
// BAD: No CRUD or FLS checks — any user can read/update regardless of permissions
|
|
18
|
+
public class AccountService {
|
|
19
|
+
public static List<Account> getAccounts() {
|
|
20
|
+
return [SELECT Id, Name, AnnualRevenue, Phone FROM Account];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static void updateRevenue(Id accountId, Decimal newRevenue) {
|
|
24
|
+
Account acc = new Account(Id = accountId, AnnualRevenue = newRevenue);
|
|
25
|
+
update acc;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### After (Secure — WITH SECURITY_ENFORCED)
|
|
31
|
+
|
|
32
|
+
```apex
|
|
33
|
+
// GOOD: CRUD/FLS enforced at the query level
|
|
34
|
+
public with sharing class AccountService {
|
|
35
|
+
public static List<Account> getAccounts() {
|
|
36
|
+
return [
|
|
37
|
+
SELECT Id, Name, AnnualRevenue, Phone
|
|
38
|
+
FROM Account
|
|
39
|
+
WITH SECURITY_ENFORCED
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static void updateRevenue(Id accountId, Decimal newRevenue) {
|
|
44
|
+
// Check field-level access before DML
|
|
45
|
+
if (!Schema.sObjectType.Account.fields.AnnualRevenue.isUpdateable()) {
|
|
46
|
+
throw new SecurityException('Insufficient access to update AnnualRevenue');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Account acc = new Account(Id = accountId, AnnualRevenue = newRevenue);
|
|
50
|
+
update acc;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public class SecurityException extends Exception {}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### After (Secure — stripInaccessible)
|
|
58
|
+
|
|
59
|
+
```apex
|
|
60
|
+
// GOOD: stripInaccessible silently removes inaccessible fields instead of throwing
|
|
61
|
+
public with sharing class AccountService {
|
|
62
|
+
public static List<Account> getAccounts() {
|
|
63
|
+
List<Account> accounts = [SELECT Id, Name, AnnualRevenue, Phone FROM Account];
|
|
64
|
+
SObjectAccessDecision decision = Security.stripInaccessible(AccessType.READABLE, accounts);
|
|
65
|
+
return (List<Account>) decision.getRecords();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public static void updateRevenue(Id accountId, Decimal newRevenue) {
|
|
69
|
+
List<Account> toUpdate = new List<Account>{
|
|
70
|
+
new Account(Id = accountId, AnnualRevenue = newRevenue)
|
|
71
|
+
};
|
|
72
|
+
SObjectAccessDecision decision = Security.stripInaccessible(AccessType.UPDATABLE, toUpdate);
|
|
73
|
+
update decision.getRecords();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## SOQL Injection Prevention
|
|
79
|
+
|
|
80
|
+
### Before (Vulnerable)
|
|
81
|
+
|
|
82
|
+
```apex
|
|
83
|
+
// BAD: User input concatenated directly into query string
|
|
84
|
+
public class AccountSearch {
|
|
85
|
+
@AuraEnabled
|
|
86
|
+
public static List<Account> search(String searchTerm) {
|
|
87
|
+
String query = 'SELECT Id, Name FROM Account WHERE Name LIKE \'%' + searchTerm + '%\'';
|
|
88
|
+
return Database.query(query);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### After (Safe — Bind Variables)
|
|
94
|
+
|
|
95
|
+
```apex
|
|
96
|
+
// GOOD: Bind variable prevents injection
|
|
97
|
+
public with sharing class AccountSearch {
|
|
98
|
+
@AuraEnabled(cacheable=true)
|
|
99
|
+
public static List<Account> search(String searchTerm) {
|
|
100
|
+
String safeTerm = '%' + String.escapeSingleQuotes(searchTerm) + '%';
|
|
101
|
+
return [
|
|
102
|
+
SELECT Id, Name
|
|
103
|
+
FROM Account
|
|
104
|
+
WHERE Name LIKE :safeTerm
|
|
105
|
+
WITH SECURITY_ENFORCED
|
|
106
|
+
LIMIT 50
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### After (Safe — Dynamic Query with Escaping)
|
|
113
|
+
|
|
114
|
+
```apex
|
|
115
|
+
// GOOD: When dynamic SOQL is unavoidable, escape input and enforce CRUD/FLS
|
|
116
|
+
public with sharing class AccountSearch {
|
|
117
|
+
@AuraEnabled(cacheable=true)
|
|
118
|
+
public static List<Account> search(String searchTerm, String sortField) {
|
|
119
|
+
// Whitelist allowed sort fields
|
|
120
|
+
Set<String> allowedSortFields = new Set<String>{ 'Name', 'CreatedDate', 'AnnualRevenue' };
|
|
121
|
+
if (!allowedSortFields.contains(sortField)) {
|
|
122
|
+
sortField = 'Name';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
String safeTerm = '%' + String.escapeSingleQuotes(searchTerm) + '%';
|
|
126
|
+
String query = 'SELECT Id, Name FROM Account'
|
|
127
|
+
+ ' WHERE Name LIKE :safeTerm'
|
|
128
|
+
+ ' WITH SECURITY_ENFORCED'
|
|
129
|
+
+ ' ORDER BY ' + sortField
|
|
130
|
+
+ ' LIMIT 50';
|
|
131
|
+
return Database.query(query);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Sharing Model Review
|
|
137
|
+
|
|
138
|
+
```apex
|
|
139
|
+
// with sharing — enforces the running user's sharing rules (default for most classes)
|
|
140
|
+
public with sharing class OpportunityService {
|
|
141
|
+
public List<Opportunity> getMyOpportunities() {
|
|
142
|
+
return [SELECT Id, Name, Amount FROM Opportunity WITH SECURITY_ENFORCED];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// without sharing — intentional escalation (document WHY)
|
|
147
|
+
// Use case: Background process that needs org-wide visibility
|
|
148
|
+
public without sharing class OpportunityRollupService {
|
|
149
|
+
/**
|
|
150
|
+
* Runs in system context because rollup calculations require
|
|
151
|
+
* access to all child records regardless of the triggering user's
|
|
152
|
+
* sharing rules. Called only from a trusted trigger handler.
|
|
153
|
+
*/
|
|
154
|
+
public static void recalculateRollups(Set<Id> accountIds) {
|
|
155
|
+
// System-level aggregation
|
|
156
|
+
List<AggregateResult> results = [
|
|
157
|
+
SELECT AccountId, SUM(Amount) totalAmount
|
|
158
|
+
FROM Opportunity
|
|
159
|
+
WHERE AccountId IN :accountIds AND IsClosed = true AND IsWon = true
|
|
160
|
+
GROUP BY AccountId
|
|
161
|
+
];
|
|
162
|
+
// ... update accounts with rollup values
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// inherited sharing — inherits context from the caller
|
|
167
|
+
// Use case: Utility classes that should respect whatever context invokes them
|
|
168
|
+
public inherited sharing class QueryHelper {
|
|
169
|
+
public static List<SObject> queryWithLimit(String objectName, Integer recordLimit) {
|
|
170
|
+
String safeObject = String.escapeSingleQuotes(objectName);
|
|
171
|
+
return Database.query(
|
|
172
|
+
'SELECT Id, Name FROM ' + safeObject + ' WITH SECURITY_ENFORCED LIMIT :recordLimit'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Running SFDX Scanner
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Install the scanner plugin (one-time setup)
|
|
182
|
+
sf plugins install @salesforce/sfdx-scanner
|
|
183
|
+
|
|
184
|
+
# Scan all Apex classes for security issues
|
|
185
|
+
sf scanner run --target "force-app/main/default/classes/**/*.cls" \
|
|
186
|
+
--category "Security" \
|
|
187
|
+
--format table
|
|
188
|
+
|
|
189
|
+
# Scan with PMD rules and generate a report
|
|
190
|
+
sf scanner run --target "force-app/main/default/classes/**/*.cls" \
|
|
191
|
+
--engine pmd \
|
|
192
|
+
--format csv \
|
|
193
|
+
--outfile scanner-results.csv
|
|
194
|
+
|
|
195
|
+
# Scan for specific security rules
|
|
196
|
+
sf scanner run --target "force-app/main/default/classes/AccountService.cls" \
|
|
197
|
+
--category "Security,Best Practices" \
|
|
198
|
+
--format table \
|
|
199
|
+
--severity-threshold 2
|
|
200
|
+
|
|
201
|
+
# Run the full AppExchange-style security review
|
|
202
|
+
sf scanner run --target "force-app/" \
|
|
203
|
+
--category "Security" \
|
|
204
|
+
--engine "pmd,retire-js" \
|
|
205
|
+
--format html \
|
|
206
|
+
--outfile security-report.html
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Security Checklist
|
|
210
|
+
|
|
211
|
+
| Check | Status | How to Verify |
|
|
212
|
+
|-------|--------|---------------|
|
|
213
|
+
| All classes use `with sharing` or document why not | | Search for `without sharing` and verify justification |
|
|
214
|
+
| SOQL uses `WITH SECURITY_ENFORCED` or `stripInaccessible` | | Search for `Database.query` and `[SELECT` without enforcement |
|
|
215
|
+
| No raw user input in SOQL/SOSL strings | | Search for string concatenation in queries |
|
|
216
|
+
| DML operations check field-level access | | Check `isCreateable()`, `isUpdateable()`, `stripInaccessible` |
|
|
217
|
+
| `@AuraEnabled` methods validate input parameters | | Review all `@AuraEnabled` methods |
|
|
218
|
+
| No hardcoded credentials or secrets | | Search for passwords, tokens, API keys in source |
|
|
219
|
+
| Named Credentials used for external callouts | | Search for `HttpRequest` and verify endpoint source |
|
|
220
|
+
| CSRF tokens on Visualforce pages | | Ensure forms are not using `GET` actions with state changes |
|
|
221
|
+
| Guest user profiles have minimal permissions | | Review site guest user profile in Setup |
|
|
222
|
+
| Sensitive data not logged or exposed in debug | | Search for `System.debug` with sensitive field names |
|
|
223
|
+
|
|
224
|
+
## Key Principles
|
|
225
|
+
|
|
226
|
+
- Default to `with sharing` on every class; only use `without sharing` with documented justification
|
|
227
|
+
- Prefer `WITH SECURITY_ENFORCED` for reads and `stripInaccessible` when you need graceful degradation
|
|
228
|
+
- Never concatenate user input into SOQL/SOSL; use bind variables or `String.escapeSingleQuotes`
|
|
229
|
+
- Whitelist dynamic field/object names rather than relying solely on escaping
|
|
230
|
+
- Run SFDX Scanner in CI to catch regressions before deployment
|
|
231
|
+
|
|
232
|
+
## Common Pitfalls
|
|
233
|
+
|
|
234
|
+
- Assuming `with sharing` enforces FLS (it only enforces record-level sharing rules, not field access)
|
|
235
|
+
- Using `String.escapeSingleQuotes` alone without also whitelisting dynamic identifiers
|
|
236
|
+
- Forgetting to add `WITH SECURITY_ENFORCED` to SOQL inside batch/schedulable classes
|
|
237
|
+
- Marking utility classes as `without sharing` out of convenience
|
|
238
|
+
- Not testing with a non-admin user profile to catch missing permissions
|
|
239
|
+
|
|
240
|
+
## SCC Skills
|
|
241
|
+
|
|
242
|
+
- `/sf-security` -- run a comprehensive security audit on your codebase
|
|
243
|
+
- `/sf-apex-best-practices` -- review Apex code including security best practices
|
|
244
|
+
- `/sf-governor-limits` -- check for governor limit issues (overlaps with security for SOQL)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Migrating Visualforce to LWC
|
|
2
|
+
|
|
3
|
+
Side-by-side examples showing how to convert a Visualforce page with an Apex controller to a Lightning Web Component using modern patterns.
|
|
4
|
+
|
|
5
|
+
## When to Use This Pattern
|
|
6
|
+
|
|
7
|
+
- Modernizing legacy Visualforce pages to Lightning Experience
|
|
8
|
+
- Converting controller-based pages to wire-service LWC components
|
|
9
|
+
- Replacing `apex:form` with `lightning-record-edit-form`
|
|
10
|
+
- Migrating page navigation from `PageReference` to `NavigationMixin`
|
|
11
|
+
- Preparing for retirement of Visualforce in Lightning-only orgs
|
|
12
|
+
|
|
13
|
+
## Original Visualforce Page
|
|
14
|
+
|
|
15
|
+
### Visualforce Markup
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<!-- AccountEditor.page -->
|
|
19
|
+
<apex:page controller="AccountEditorController" lightningStylesheets="true">
|
|
20
|
+
<apex:form>
|
|
21
|
+
<apex:pageBlock title="Edit Account" mode="edit">
|
|
22
|
+
<apex:pageMessages />
|
|
23
|
+
|
|
24
|
+
<apex:pageBlockButtons>
|
|
25
|
+
<apex:commandButton action="{!save}" value="Save" />
|
|
26
|
+
<apex:commandButton action="{!cancel}" value="Cancel" immediate="true" />
|
|
27
|
+
</apex:pageBlockButtons>
|
|
28
|
+
|
|
29
|
+
<apex:pageBlockSection columns="2">
|
|
30
|
+
<apex:inputField value="{!account.Name}" required="true" />
|
|
31
|
+
<apex:inputField value="{!account.Industry}" />
|
|
32
|
+
<apex:inputField value="{!account.Phone}" />
|
|
33
|
+
<apex:inputField value="{!account.Website}" />
|
|
34
|
+
<apex:inputField value="{!account.AnnualRevenue}" />
|
|
35
|
+
<apex:inputField value="{!account.Description}" />
|
|
36
|
+
</apex:pageBlockSection>
|
|
37
|
+
|
|
38
|
+
<apex:pageBlockSection title="Related Contacts" columns="1">
|
|
39
|
+
<apex:pageBlockTable value="{!contacts}" var="c">
|
|
40
|
+
<apex:column value="{!c.Name}" />
|
|
41
|
+
<apex:column value="{!c.Email}" />
|
|
42
|
+
<apex:column value="{!c.Phone}" />
|
|
43
|
+
</apex:pageBlockTable>
|
|
44
|
+
</apex:pageBlockSection>
|
|
45
|
+
</apex:pageBlock>
|
|
46
|
+
</apex:form>
|
|
47
|
+
</apex:page>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Visualforce Controller
|
|
51
|
+
|
|
52
|
+
```apex
|
|
53
|
+
public with sharing class AccountEditorController {
|
|
54
|
+
public Account account { get; set; }
|
|
55
|
+
public List<Contact> contacts { get; set; }
|
|
56
|
+
|
|
57
|
+
public AccountEditorController() {
|
|
58
|
+
Id accountId = ApexPages.currentPage().getParameters().get('id');
|
|
59
|
+
if (accountId != null) {
|
|
60
|
+
account = [
|
|
61
|
+
SELECT Id, Name, Industry, Phone, Website, AnnualRevenue, Description
|
|
62
|
+
FROM Account WHERE Id = :accountId
|
|
63
|
+
];
|
|
64
|
+
contacts = [
|
|
65
|
+
SELECT Id, Name, Email, Phone
|
|
66
|
+
FROM Contact WHERE AccountId = :accountId
|
|
67
|
+
ORDER BY Name
|
|
68
|
+
];
|
|
69
|
+
} else {
|
|
70
|
+
account = new Account();
|
|
71
|
+
contacts = new List<Contact>();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public PageReference save() {
|
|
76
|
+
try {
|
|
77
|
+
upsert account;
|
|
78
|
+
return new PageReference('/' + account.Id);
|
|
79
|
+
} catch (DmlException e) {
|
|
80
|
+
ApexPages.addMessages(e);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public PageReference cancel() {
|
|
86
|
+
if (account.Id != null) {
|
|
87
|
+
return new PageReference('/' + account.Id);
|
|
88
|
+
}
|
|
89
|
+
return new PageReference('/001'); // Account list view
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Equivalent LWC Component
|
|
95
|
+
|
|
96
|
+
### LWC HTML
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<!-- accountEditor.html -->
|
|
100
|
+
<template>
|
|
101
|
+
<lightning-card title="Edit Account" icon-name="standard:account">
|
|
102
|
+
<!-- Record Edit Form replaces apex:form + apex:inputField -->
|
|
103
|
+
<lightning-record-edit-form
|
|
104
|
+
record-id={recordId}
|
|
105
|
+
object-api-name="Account"
|
|
106
|
+
onsuccess={handleSuccess}
|
|
107
|
+
onerror={handleError}>
|
|
108
|
+
|
|
109
|
+
<lightning-messages></lightning-messages>
|
|
110
|
+
|
|
111
|
+
<div class="slds-grid slds-wrap slds-gutters">
|
|
112
|
+
<div class="slds-col slds-size_1-of-2 slds-p-around_small">
|
|
113
|
+
<lightning-input-field field-name="Name" required></lightning-input-field>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="slds-col slds-size_1-of-2 slds-p-around_small">
|
|
116
|
+
<lightning-input-field field-name="Industry"></lightning-input-field>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="slds-col slds-size_1-of-2 slds-p-around_small">
|
|
119
|
+
<lightning-input-field field-name="Phone"></lightning-input-field>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="slds-col slds-size_1-of-2 slds-p-around_small">
|
|
122
|
+
<lightning-input-field field-name="Website"></lightning-input-field>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="slds-col slds-size_1-of-2 slds-p-around_small">
|
|
125
|
+
<lightning-input-field field-name="AnnualRevenue"></lightning-input-field>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="slds-col slds-size_1-of-1 slds-p-around_small">
|
|
128
|
+
<lightning-input-field field-name="Description"></lightning-input-field>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="slds-m-top_medium slds-p-around_small">
|
|
133
|
+
<lightning-button variant="brand" type="submit" label="Save"></lightning-button>
|
|
134
|
+
<lightning-button label="Cancel" onclick={handleCancel} class="slds-m-left_x-small"></lightning-button>
|
|
135
|
+
</div>
|
|
136
|
+
</lightning-record-edit-form>
|
|
137
|
+
|
|
138
|
+
<!-- Related Contacts section -->
|
|
139
|
+
<template if:true={contacts.data}>
|
|
140
|
+
<div class="slds-p-around_small slds-m-top_medium">
|
|
141
|
+
<h2 class="slds-text-heading_small slds-m-bottom_small">Related Contacts</h2>
|
|
142
|
+
<lightning-datatable
|
|
143
|
+
key-field="Id"
|
|
144
|
+
data={contacts.data}
|
|
145
|
+
columns={contactColumns}
|
|
146
|
+
hide-checkbox-column>
|
|
147
|
+
</lightning-datatable>
|
|
148
|
+
</div>
|
|
149
|
+
</template>
|
|
150
|
+
</lightning-card>
|
|
151
|
+
</template>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### LWC JavaScript
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
// accountEditor.js
|
|
158
|
+
import { LightningElement, api, wire } from 'lwc';
|
|
159
|
+
import { NavigationMixin } from 'lightning/navigation';
|
|
160
|
+
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
|
|
161
|
+
import getRelatedContacts from '@salesforce/apex/AccountEditorLwcController.getRelatedContacts';
|
|
162
|
+
|
|
163
|
+
const CONTACT_COLUMNS = [
|
|
164
|
+
{ label: 'Name', fieldName: 'Name', type: 'text' },
|
|
165
|
+
{ label: 'Email', fieldName: 'Email', type: 'email' },
|
|
166
|
+
{ label: 'Phone', fieldName: 'Phone', type: 'phone' }
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
export default class AccountEditor extends NavigationMixin(LightningElement) {
|
|
170
|
+
@api recordId;
|
|
171
|
+
contactColumns = CONTACT_COLUMNS;
|
|
172
|
+
|
|
173
|
+
@wire(getRelatedContacts, { accountId: '$recordId' })
|
|
174
|
+
contacts;
|
|
175
|
+
|
|
176
|
+
handleSuccess(event) {
|
|
177
|
+
this.dispatchEvent(
|
|
178
|
+
new ShowToastEvent({
|
|
179
|
+
title: 'Success',
|
|
180
|
+
message: 'Account saved successfully',
|
|
181
|
+
variant: 'success'
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Navigate to the record page (replaces PageReference)
|
|
186
|
+
this[NavigationMixin.Navigate]({
|
|
187
|
+
type: 'standard__recordPage',
|
|
188
|
+
attributes: {
|
|
189
|
+
recordId: event.detail.id,
|
|
190
|
+
objectApiName: 'Account',
|
|
191
|
+
actionName: 'view'
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
handleError(event) {
|
|
197
|
+
this.dispatchEvent(
|
|
198
|
+
new ShowToastEvent({
|
|
199
|
+
title: 'Error',
|
|
200
|
+
message: event.detail.message,
|
|
201
|
+
variant: 'error'
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
handleCancel() {
|
|
207
|
+
if (this.recordId) {
|
|
208
|
+
// Navigate back to the record (replaces PageReference)
|
|
209
|
+
this[NavigationMixin.Navigate]({
|
|
210
|
+
type: 'standard__recordPage',
|
|
211
|
+
attributes: {
|
|
212
|
+
recordId: this.recordId,
|
|
213
|
+
objectApiName: 'Account',
|
|
214
|
+
actionName: 'view'
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
// Navigate to the Account list view (replaces PageReference('/001'))
|
|
219
|
+
this[NavigationMixin.Navigate]({
|
|
220
|
+
type: 'standard__objectPage',
|
|
221
|
+
attributes: {
|
|
222
|
+
objectApiName: 'Account',
|
|
223
|
+
actionName: 'list'
|
|
224
|
+
},
|
|
225
|
+
state: {
|
|
226
|
+
filterName: 'Recent'
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### LWC Metadata
|
|
235
|
+
|
|
236
|
+
```xml
|
|
237
|
+
<!-- accountEditor.js-meta.xml -->
|
|
238
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
239
|
+
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
240
|
+
<apiVersion>66.0</apiVersion>
|
|
241
|
+
<isExposed>true</isExposed>
|
|
242
|
+
<targets>
|
|
243
|
+
<target>lightning__RecordPage</target>
|
|
244
|
+
<target>lightning__AppPage</target>
|
|
245
|
+
</targets>
|
|
246
|
+
<targetConfigs>
|
|
247
|
+
<targetConfig targets="lightning__RecordPage">
|
|
248
|
+
<objects>
|
|
249
|
+
<object>Account</object>
|
|
250
|
+
</objects>
|
|
251
|
+
</targetConfig>
|
|
252
|
+
</targetConfigs>
|
|
253
|
+
</LightningComponentBundle>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Apex Controller (Simplified)
|
|
257
|
+
|
|
258
|
+
```apex
|
|
259
|
+
public with sharing class AccountEditorLwcController {
|
|
260
|
+
@AuraEnabled(cacheable=true)
|
|
261
|
+
public static List<Contact> getRelatedContacts(Id accountId) {
|
|
262
|
+
return [
|
|
263
|
+
SELECT Id, Name, Email, Phone
|
|
264
|
+
FROM Contact
|
|
265
|
+
WHERE AccountId = :accountId
|
|
266
|
+
WITH SECURITY_ENFORCED
|
|
267
|
+
ORDER BY Name
|
|
268
|
+
LIMIT 100
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Migration Mapping Reference
|
|
275
|
+
|
|
276
|
+
| Visualforce | LWC Equivalent |
|
|
277
|
+
|-------------|----------------|
|
|
278
|
+
| `apex:page` | LWC component with `lightning-card` |
|
|
279
|
+
| `apex:form` | `lightning-record-edit-form` |
|
|
280
|
+
| `apex:inputField` | `lightning-input-field` |
|
|
281
|
+
| `apex:commandButton action="{!save}"` | `lightning-button type="submit"` |
|
|
282
|
+
| `apex:pageMessages` | `lightning-messages` |
|
|
283
|
+
| `apex:pageBlockTable` | `lightning-datatable` |
|
|
284
|
+
| `ApexPages.addMessages(e)` | `ShowToastEvent` |
|
|
285
|
+
| `new PageReference('/id')` | `NavigationMixin.Navigate` |
|
|
286
|
+
| Controller constructor query | `@wire` with `@AuraEnabled(cacheable=true)` |
|
|
287
|
+
| `ApexPages.currentPage().getParameters().get('id')` | `@api recordId` |
|
|
288
|
+
| `apex:outputPanel rendered="{!condition}"` | `template if:true={condition}` |
|
|
289
|
+
| `apex:repeat` | `template for:each={items}` |
|
|
290
|
+
| `apex:actionFunction` | Imperative Apex call |
|
|
291
|
+
|
|
292
|
+
## Key Principles
|
|
293
|
+
|
|
294
|
+
- Use `lightning-record-edit-form` instead of custom save logic when editing standard/custom object fields
|
|
295
|
+
- Replace all `PageReference` navigation with `NavigationMixin` for Lightning Experience compatibility
|
|
296
|
+
- Use `@wire` for read operations and imperative calls for mutations
|
|
297
|
+
- Move field-level rendering logic from controller to reactive properties in JS
|
|
298
|
+
- LWC automatically handles CRUD/FLS when using `lightning-record-edit-form`
|
|
299
|
+
|
|
300
|
+
## Common Pitfalls
|
|
301
|
+
|
|
302
|
+
- Forgetting to extend `NavigationMixin(LightningElement)` before calling Navigate
|
|
303
|
+
- Using imperative Apex for cacheable reads instead of `@wire` (loses caching and reactivity)
|
|
304
|
+
- Not adding `WITH SECURITY_ENFORCED` in the LWC controller Apex methods
|
|
305
|
+
- Trying to replicate the exact VF layout instead of adopting SLDS grid patterns
|
|
306
|
+
- Hardcoding record type IDs or key prefixes (like `/001`) that were common in VF pages
|
|
307
|
+
- Missing the `isExposed` and `targets` configuration in the meta XML
|
|
308
|
+
|
|
309
|
+
## SCC Skills
|
|
310
|
+
|
|
311
|
+
- `/sf-visualforce-development` -- audit a Visualforce page for migration readiness
|
|
312
|
+
- `/sf-lwc-development` -- review the migrated LWC component for best practices
|
|
313
|
+
- `/sf-apex-best-practices` -- review the simplified Apex controller
|
|
314
|
+
- `/sf-security` -- verify security enforcement in the new component
|