acek-skills 1.0.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/LICENSE +21 -0
- package/README.md +268 -0
- package/bin/cli.js +300 -0
- package/package.json +33 -0
- package/skills/sf-admin/SKILL.md +245 -0
- package/skills/sf-ba/SKILL.md +297 -0
- package/skills/sf-data-migration/SKILL.md +374 -0
- package/skills/sf-dev/SKILL.md +328 -0
- package/skills/sf-devops/SKILL.md +372 -0
- package/skills/sf-ideation/SKILL.md +132 -0
- package/skills/sf-security-review/SKILL.md +320 -0
- package/skills/sf-testing/SKILL.md +361 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-data-migration
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill for Salesforce data migration, bulk data operations, and data management tasks:
|
|
5
|
+
designing import/export strategies, writing Data Loader automation scripts, upsert operations
|
|
6
|
+
with External IDs, bulk SOQL exports, data cleansing before migration, error handling for bulk
|
|
7
|
+
loads, and generating data migration plans. Trigger when the user mentions "data migration",
|
|
8
|
+
"bulk import", "data loader", "upsert", "external ID", "migrate records", "export data",
|
|
9
|
+
"data cleansing", "bulk delete", "mass update", or asks how to move large volumes of records
|
|
10
|
+
between orgs or from external systems into Salesforce.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Salesforce Data Migration Skill
|
|
14
|
+
|
|
15
|
+
## Environment Context
|
|
16
|
+
- API Version: **61.0**
|
|
17
|
+
- Tooling: **SF CLI** (`sf data` commands) for dev/test volumes; Data Loader for production volumes
|
|
18
|
+
- Always migrate to **sandbox first**, verify, then production
|
|
19
|
+
- Single org: sandbox → production
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Migration Planning — Before Touching Any Data
|
|
24
|
+
|
|
25
|
+
### Step 1: Discovery Checklist
|
|
26
|
+
- [ ] What objects and how many records? (run COUNT queries first)
|
|
27
|
+
- [ ] Are there required fields that source data may not have?
|
|
28
|
+
- [ ] Are there lookup/relationship fields? (parent records must exist first)
|
|
29
|
+
- [ ] Are there Record Types? (map source values to Salesforce Record Type IDs)
|
|
30
|
+
- [ ] Are there Validation Rules that will block import? (consider deactivating temporarily)
|
|
31
|
+
- [ ] Are there Triggers/Flows that fire on insert/update? (decide: bypass or let fire?)
|
|
32
|
+
- [ ] Does the source data have a unique identifier for upsert (External ID)?
|
|
33
|
+
- [ ] What is the target volume? (under 10K → SF CLI; over 10K → Data Loader / Bulk API)
|
|
34
|
+
|
|
35
|
+
### Step 2: Dependency Order
|
|
36
|
+
Always load in parent → child order:
|
|
37
|
+
```
|
|
38
|
+
1. Custom Metadata / Reference Data (e.g. picklist-driving records)
|
|
39
|
+
2. Accounts
|
|
40
|
+
3. Contacts (requires Account)
|
|
41
|
+
4. Opportunities (requires Account)
|
|
42
|
+
5. Opportunity Products (requires Opportunity + Pricebook Entry)
|
|
43
|
+
6. Cases (requires Account + Contact)
|
|
44
|
+
7. Custom Objects (based on lookup dependency)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Step 3: External ID Strategy
|
|
48
|
+
Every migrated object should have an External ID field to enable upsert and relationship mapping.
|
|
49
|
+
|
|
50
|
+
```apex
|
|
51
|
+
// External ID field naming convention:
|
|
52
|
+
ExternalId__c // generic
|
|
53
|
+
LegacySystemId__c // named after source system
|
|
54
|
+
SapId__c, OracleId__c // system-specific
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Field settings:
|
|
58
|
+
- Type: Text (or Number if IDs are numeric)
|
|
59
|
+
- Mark as: **External ID** ✓ and **Unique** ✓
|
|
60
|
+
- Max length: match source system ID length
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## SF CLI Data Commands — Dev/Test Volumes (<50K records)
|
|
65
|
+
|
|
66
|
+
### Query & Export
|
|
67
|
+
```bash
|
|
68
|
+
# Basic query to CSV
|
|
69
|
+
sf data query \
|
|
70
|
+
--query "SELECT Id, Name, Industry, Phone FROM Account WHERE CreatedDate = THIS_YEAR" \
|
|
71
|
+
--target-org <alias> \
|
|
72
|
+
--result-format csv > exports/accounts.csv
|
|
73
|
+
|
|
74
|
+
# Export with relationships
|
|
75
|
+
sf data query \
|
|
76
|
+
--query "SELECT Id, Name, Account.Name, Email FROM Contact WHERE AccountId != null" \
|
|
77
|
+
--target-org <alias> \
|
|
78
|
+
--result-format csv > exports/contacts.csv
|
|
79
|
+
|
|
80
|
+
# Count records before migration
|
|
81
|
+
sf data query \
|
|
82
|
+
--query "SELECT COUNT() FROM Account" \
|
|
83
|
+
--target-org <alias>
|
|
84
|
+
|
|
85
|
+
# Export with Bulk API (for larger volumes)
|
|
86
|
+
sf data export bulk \
|
|
87
|
+
--query "SELECT Id, Name, ExternalId__c FROM Account" \
|
|
88
|
+
--output-file exports/accounts-bulk.csv \
|
|
89
|
+
--target-org <alias>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Import / Upsert
|
|
93
|
+
```bash
|
|
94
|
+
# Insert new records
|
|
95
|
+
sf data import bulk \
|
|
96
|
+
--sobject Account \
|
|
97
|
+
--file imports/accounts.csv \
|
|
98
|
+
--target-org <alias>
|
|
99
|
+
|
|
100
|
+
# Upsert (insert + update based on External ID)
|
|
101
|
+
sf data upsert bulk \
|
|
102
|
+
--sobject Account \
|
|
103
|
+
--file imports/accounts.csv \
|
|
104
|
+
--external-id ExternalId__c \
|
|
105
|
+
--target-org <alias>
|
|
106
|
+
|
|
107
|
+
# Delete records
|
|
108
|
+
sf data delete bulk \
|
|
109
|
+
--sobject Account \
|
|
110
|
+
--file exports/accounts-to-delete.csv \
|
|
111
|
+
--target-org <alias>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Check Job Status
|
|
115
|
+
```bash
|
|
116
|
+
sf data bulk results \
|
|
117
|
+
--job-id <JobId> \
|
|
118
|
+
--target-org <alias>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Data Loader — Production Volumes (>50K records)
|
|
124
|
+
|
|
125
|
+
Data Loader uses Bulk API 2.0 by default. Use for production migration.
|
|
126
|
+
|
|
127
|
+
### CSV Requirements
|
|
128
|
+
- UTF-8 encoding (no BOM)
|
|
129
|
+
- First row: field API names (e.g. `Name,ExternalId__c,Industry__c`)
|
|
130
|
+
- `null` to clear a field: leave cell empty OR use `#N/A` for some fields
|
|
131
|
+
- Date format: `YYYY-MM-DDThh:mm:ss.000Z` (ISO 8601)
|
|
132
|
+
- Boolean: `true` / `false` (lowercase)
|
|
133
|
+
- Currency: numeric only, no currency symbols
|
|
134
|
+
|
|
135
|
+
### Batch Size Recommendations
|
|
136
|
+
| Volume | Batch Size | Concurrency |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| < 10K | 200 | Serial |
|
|
139
|
+
| 10K – 100K | 2,000 | Parallel |
|
|
140
|
+
| 100K – 1M | 10,000 | Parallel |
|
|
141
|
+
| > 1M | 10,000 | Parallel + schedule overnight |
|
|
142
|
+
|
|
143
|
+
### Error Handling
|
|
144
|
+
- Always save the **success file** and **error file** per run
|
|
145
|
+
- Error file contains failed rows + error message — fix and re-run only errors
|
|
146
|
+
- Common errors and fixes:
|
|
147
|
+
|
|
148
|
+
| Error | Cause | Fix |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `REQUIRED_FIELD_MISSING` | Required field empty in CSV | Fill required fields; check validation rules |
|
|
151
|
+
| `INVALID_FIELD` | Field API name wrong | Check with `sf sobject describe --sobject <Object>` |
|
|
152
|
+
| `FIELD_INTEGRITY_EXCEPTION` | Lookup ID doesn't exist in org | Load parent records first |
|
|
153
|
+
| `DUPLICATE_VALUE` | Unique field already exists | Switch from insert to upsert with External ID |
|
|
154
|
+
| `STRING_TOO_LONG` | Value exceeds field length | Truncate or increase field length |
|
|
155
|
+
| `CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY` | Trigger/validation blocking | Check trigger logic; may need to deactivate VR temporarily |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Upsert Pattern — External ID Relationship Mapping
|
|
160
|
+
|
|
161
|
+
When loading child records and parent exists via External ID (not Salesforce ID):
|
|
162
|
+
|
|
163
|
+
### CSV Format
|
|
164
|
+
Instead of using Salesforce `AccountId`, reference the parent's External ID:
|
|
165
|
+
|
|
166
|
+
```csv
|
|
167
|
+
Name,Email,Account.ExternalId__c
|
|
168
|
+
John Doe,john@example.com,ACC-12345
|
|
169
|
+
Jane Smith,jane@example.com,ACC-67890
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Salesforce resolves `Account.ExternalId__c` to the Account's Salesforce ID automatically during upsert.
|
|
173
|
+
|
|
174
|
+
### Upsert Command
|
|
175
|
+
```bash
|
|
176
|
+
sf data upsert bulk \
|
|
177
|
+
--sobject Contact \
|
|
178
|
+
--file imports/contacts-with-account-ref.csv \
|
|
179
|
+
--external-id ExternalId__c \
|
|
180
|
+
--target-org <alias>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Data Cleansing — Before Migration
|
|
186
|
+
|
|
187
|
+
### Common Issues to Fix in Source Data
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# Python script example — clean CSV before Salesforce import
|
|
191
|
+
|
|
192
|
+
import pandas as pd
|
|
193
|
+
|
|
194
|
+
df = pd.read_csv('raw_export.csv')
|
|
195
|
+
|
|
196
|
+
# Remove duplicates on External ID
|
|
197
|
+
df = df.drop_duplicates(subset=['external_id'])
|
|
198
|
+
|
|
199
|
+
# Truncate strings to Salesforce field limits
|
|
200
|
+
df['name'] = df['name'].str[:255] # Text field max
|
|
201
|
+
df['description'] = df['description'].str[:32000] # Long Text max
|
|
202
|
+
|
|
203
|
+
# Normalize phone numbers
|
|
204
|
+
df['phone'] = df['phone'].str.replace(r'\D', '', regex=True)
|
|
205
|
+
|
|
206
|
+
# Map picklist values to Salesforce API values
|
|
207
|
+
industry_map = {
|
|
208
|
+
'Tech': 'Technology',
|
|
209
|
+
'Fin': 'Finance',
|
|
210
|
+
'Health': 'Healthcare'
|
|
211
|
+
}
|
|
212
|
+
df['industry'] = df['industry'].map(industry_map)
|
|
213
|
+
|
|
214
|
+
# Convert date format to ISO 8601
|
|
215
|
+
df['created_date'] = pd.to_datetime(df['created_date']).dt.strftime('%Y-%m-%dT%H:%M:%S.000Z')
|
|
216
|
+
|
|
217
|
+
# Replace NaN with empty string
|
|
218
|
+
df = df.fillna('')
|
|
219
|
+
|
|
220
|
+
df.to_csv('cleaned_export.csv', index=False, encoding='utf-8')
|
|
221
|
+
print(f"Cleaned: {len(df)} records")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### SOQL-Based Deduplication Check
|
|
225
|
+
```bash
|
|
226
|
+
# Find duplicates in Salesforce by External ID
|
|
227
|
+
sf data query \
|
|
228
|
+
--query "SELECT ExternalId__c, COUNT(Id) total FROM Account GROUP BY ExternalId__c HAVING COUNT(Id) > 1" \
|
|
229
|
+
--target-org <alias>
|
|
230
|
+
|
|
231
|
+
# Find records without External ID (not upsert-safe)
|
|
232
|
+
sf data query \
|
|
233
|
+
--query "SELECT Id, Name FROM Account WHERE ExternalId__c = null LIMIT 200" \
|
|
234
|
+
--target-org <alias>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Apex Batch for Large-Scale Updates
|
|
240
|
+
|
|
241
|
+
When you need to transform/update data already in Salesforce at scale:
|
|
242
|
+
|
|
243
|
+
```apex
|
|
244
|
+
public class AccountMigrationBatch implements Database.Batchable<SObject>, Database.Stateful {
|
|
245
|
+
private Integer successCount = 0;
|
|
246
|
+
private Integer errorCount = 0;
|
|
247
|
+
private List<String> errors = new List<String>();
|
|
248
|
+
|
|
249
|
+
public Database.QueryLocator start(Database.BatchableContext bc) {
|
|
250
|
+
return Database.getQueryLocator(
|
|
251
|
+
'SELECT Id, Name, LegacyStatus__c, Status__c ' +
|
|
252
|
+
'FROM Account WHERE MigrationComplete__c = false'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public void execute(Database.BatchableContext bc, List<Account> scope) {
|
|
257
|
+
List<Account> toUpdate = new List<Account>();
|
|
258
|
+
|
|
259
|
+
for (Account acc : scope) {
|
|
260
|
+
// Transform logic
|
|
261
|
+
acc.Status__c = mapStatus(acc.LegacyStatus__c);
|
|
262
|
+
acc.MigrationComplete__c = true;
|
|
263
|
+
toUpdate.add(acc);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
Database.SaveResult[] results = Database.update(toUpdate, false); // allOrNone=false
|
|
267
|
+
for (Integer i = 0; i < results.size(); i++) {
|
|
268
|
+
if (results[i].isSuccess()) {
|
|
269
|
+
successCount++;
|
|
270
|
+
} else {
|
|
271
|
+
errorCount++;
|
|
272
|
+
errors.add(toUpdate[i].Id + ': ' + results[i].getErrors()[0].getMessage());
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public void finish(Database.BatchableContext bc) {
|
|
278
|
+
// Send summary email or create log record
|
|
279
|
+
System.debug('Migration complete. Success: ' + successCount + ', Errors: ' + errorCount);
|
|
280
|
+
for (String err : errors) {
|
|
281
|
+
System.debug('ERROR: ' + err);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private String mapStatus(String legacyStatus) {
|
|
286
|
+
Map<String, String> statusMap = new Map<String, String>{
|
|
287
|
+
'A' => 'Active',
|
|
288
|
+
'I' => 'Inactive',
|
|
289
|
+
'P' => 'Pending'
|
|
290
|
+
};
|
|
291
|
+
return statusMap.containsKey(legacyStatus) ? statusMap.get(legacyStatus) : 'Unknown';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Execute Batch
|
|
297
|
+
```apex
|
|
298
|
+
// In Anonymous Apex
|
|
299
|
+
Database.executeBatch(new AccountMigrationBatch(), 200); // 200 records per batch
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Migration Runbook Template
|
|
305
|
+
|
|
306
|
+
File: `instructions/migration/YYYYMMDD-<migration-name>-runbook.md`
|
|
307
|
+
|
|
308
|
+
```markdown
|
|
309
|
+
# Migration Runbook: [Migration Name]
|
|
310
|
+
|
|
311
|
+
**Date:** YYYY-MM-DD
|
|
312
|
+
**Target Org:** [{{DEV_ORG_ALIAS}} / {{PROD_ORG_ALIAS}}]
|
|
313
|
+
**Estimated Volume:** [X records across Y objects]
|
|
314
|
+
**Estimated Duration:** [X hours]
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Pre-Migration Checklist
|
|
319
|
+
- [ ] Backup current data (export all affected objects)
|
|
320
|
+
- [ ] Verify External ID field exists on all target objects
|
|
321
|
+
- [ ] Test migration in sandbox with 10% sample — confirm success rate > 99%
|
|
322
|
+
- [ ] Identify Validation Rules to deactivate temporarily (list below)
|
|
323
|
+
- [ ] Identify Triggers/Flows that will fire — confirm acceptable behavior
|
|
324
|
+
- [ ] Communicate downtime window to affected users (if any)
|
|
325
|
+
- [ ] Confirm Data Loader / SF CLI version is current
|
|
326
|
+
|
|
327
|
+
## Validation Rules to Deactivate During Migration
|
|
328
|
+
| Object | Validation Rule | Reason to Deactivate |
|
|
329
|
+
|---|---|---|
|
|
330
|
+
| Account | ACCOUNT_RequireIndustry | Source data may not have Industry |
|
|
331
|
+
|
|
332
|
+
## Load Order
|
|
333
|
+
1. [Object 1] — [file name] — [estimated records]
|
|
334
|
+
2. [Object 2] — [file name] — [estimated records]
|
|
335
|
+
|
|
336
|
+
## Rollback Plan
|
|
337
|
+
If error rate > 5% or critical data issue found:
|
|
338
|
+
1. Stop migration immediately
|
|
339
|
+
2. Export all records inserted this session (filter by CreatedDate = TODAY)
|
|
340
|
+
3. Delete inserted records using error-free export CSV + sf data delete bulk
|
|
341
|
+
4. Restore Validation Rules
|
|
342
|
+
5. Investigate root cause before re-attempting
|
|
343
|
+
|
|
344
|
+
## Post-Migration Verification
|
|
345
|
+
- [ ] Record counts match source system
|
|
346
|
+
- [ ] Sample 10 records manually — verify all fields correct
|
|
347
|
+
- [ ] Lookup relationships intact (spot-check Contacts → Accounts)
|
|
348
|
+
- [ ] Validation Rules re-activated
|
|
349
|
+
- [ ] Users notified migration complete
|
|
350
|
+
|
|
351
|
+
## Results
|
|
352
|
+
| Object | Inserted | Updated | Failed | Notes |
|
|
353
|
+
|---|---|---|---|---|
|
|
354
|
+
| Account | | | | |
|
|
355
|
+
| Contact | | | | |
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## SF CLI Quick Reference — Data Commands
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# Describe object fields (use to verify API names before import)
|
|
364
|
+
sf sobject describe --sobject Account --target-org <alias>
|
|
365
|
+
|
|
366
|
+
# Query to verify post-migration counts
|
|
367
|
+
sf data query --query "SELECT COUNT() FROM Account WHERE ExternalId__c != null" --target-org <alias>
|
|
368
|
+
|
|
369
|
+
# Get Bulk API job results
|
|
370
|
+
sf data bulk results --job-id <JobId> --target-org <alias>
|
|
371
|
+
|
|
372
|
+
# List all Bulk API jobs (recent)
|
|
373
|
+
sf data bulk status --target-org <alias>
|
|
374
|
+
```
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-dev
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill for ANY Salesforce custom development task: writing or reviewing Apex classes,
|
|
5
|
+
triggers, batch jobs, test classes, SOQL queries, LWC components (HTML/JS/CSS), Aura components,
|
|
6
|
+
and integration patterns. Trigger this skill whenever the user mentions Apex, LWC, SOQL, triggers,
|
|
7
|
+
@AuraEnabled, @wire, REST/SOAP callouts, platform events, or asks to build/fix/refactor any
|
|
8
|
+
Salesforce code. Also use when the user asks about governor limits, test coverage, or debugging
|
|
9
|
+
Salesforce-specific runtime errors.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Salesforce Developer Skill
|
|
13
|
+
|
|
14
|
+
## Environment Context
|
|
15
|
+
- API Version: **61.0**
|
|
16
|
+
- Tooling: **SF CLI** (`sf` commands)
|
|
17
|
+
- Apex LSP: **offline** — never use compile-time SObject type references that require org metadata
|
|
18
|
+
- Version control: **GitHub** (branching: `main → staging → develop → feature/*`)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Apex — Core Rules
|
|
23
|
+
|
|
24
|
+
### Governor Limits (non-negotiable)
|
|
25
|
+
- **No SOQL inside loops** — always bulkify; query outside the loop, store in Map/List
|
|
26
|
+
- **No DML inside loops** — collect records, DML once after loop
|
|
27
|
+
- **No callouts inside loops**
|
|
28
|
+
- Minimum test coverage: **85%** (aim for 90%+)
|
|
29
|
+
|
|
30
|
+
### Security Patterns
|
|
31
|
+
```apex
|
|
32
|
+
// ✅ CRUD check — use Schema.getGlobalDescribe() (LSP-safe)
|
|
33
|
+
if (!Schema.getGlobalDescribe().get('ObjectName').getDescribe().isCreateable()) {
|
|
34
|
+
throw new AuraHandledException('Insufficient permissions.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ❌ NEVER use compile-time reference (LSP offline can't resolve)
|
|
38
|
+
// if (!ContentVersion.SObjectType.getDescribe().isCreateable()) { ... }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `ContentDocumentLink`: skip explicit CRUD check — junction object protected by `with sharing` + try-catch
|
|
42
|
+
- FLS check via `evDescribe.fields` not available offline — use object-level `isUpdateable()` only
|
|
43
|
+
- All classes handling user data: use **`with sharing`**
|
|
44
|
+
|
|
45
|
+
### @AuraEnabled Methods
|
|
46
|
+
```apex
|
|
47
|
+
@AuraEnabled
|
|
48
|
+
public static Result myMethod(String param) {
|
|
49
|
+
try {
|
|
50
|
+
// business logic
|
|
51
|
+
} catch (Exception e) {
|
|
52
|
+
throw new AuraHandledException(e.getMessage());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
- Always wrap DML in try-catch → re-throw as `AuraHandledException`
|
|
57
|
+
- Private helper methods: throw `CalloutException` (caller wraps it)
|
|
58
|
+
|
|
59
|
+
### Exception Behavior (Salesforce by design)
|
|
60
|
+
- `AuraHandledException.getMessage()` in test context **always** returns `'Script-thrown exception'`
|
|
61
|
+
- In test assertions: `System.assertEquals('Script-thrown exception', e.getMessage())` — **do not change this**
|
|
62
|
+
|
|
63
|
+
### Naming Conventions
|
|
64
|
+
| Type | Convention | Example |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| Apex Class | PascalCase | `AccountHelper`, `OpportunityTrigger` |
|
|
67
|
+
| Test Class | PascalCase + `Test` suffix | `AccountHelperTest` |
|
|
68
|
+
| Custom Field | PascalCase__c | `ProjectStatus__c` |
|
|
69
|
+
| Variable | camelCase | `accountList`, `oppMap` |
|
|
70
|
+
|
|
71
|
+
### ApexDoc (required on all public methods)
|
|
72
|
+
```apex
|
|
73
|
+
/**
|
|
74
|
+
* @description Brief description of what this method does
|
|
75
|
+
* @param recordId The ID of the record to process
|
|
76
|
+
* @return List of processed results
|
|
77
|
+
* @throws AuraHandledException if user lacks required permissions
|
|
78
|
+
*/
|
|
79
|
+
@AuraEnabled
|
|
80
|
+
public static List<Result__c> processRecord(Id recordId) { ... }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Test Class Pattern
|
|
84
|
+
```apex
|
|
85
|
+
@isTest
|
|
86
|
+
private class AccountHelperTest {
|
|
87
|
+
@TestSetup
|
|
88
|
+
static void makeData() {
|
|
89
|
+
// create test records once for all test methods
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@isTest
|
|
93
|
+
static void testPositivePath() {
|
|
94
|
+
Test.startTest();
|
|
95
|
+
// call method
|
|
96
|
+
Test.stopTest();
|
|
97
|
+
// assert
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@isTest
|
|
101
|
+
static void testNegativePath_insufficientPermission() {
|
|
102
|
+
// test exception path
|
|
103
|
+
try {
|
|
104
|
+
AccountHelper.myMethod(null);
|
|
105
|
+
System.assert(false, 'Expected exception not thrown');
|
|
106
|
+
} catch (AuraHandledException e) {
|
|
107
|
+
System.assertEquals('Script-thrown exception', e.getMessage());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## SOQL — Best Practices
|
|
116
|
+
|
|
117
|
+
```apex
|
|
118
|
+
// ✅ Bulkified pattern
|
|
119
|
+
Map<Id, Account> accountMap = new Map<Id, Account>(
|
|
120
|
+
[SELECT Id, Name, OwnerId FROM Account WHERE Id IN :accountIds]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ✅ Aggregate query
|
|
124
|
+
List<AggregateResult> results = [
|
|
125
|
+
SELECT StageName, COUNT(Id) total
|
|
126
|
+
FROM Opportunity
|
|
127
|
+
WHERE CloseDate = THIS_YEAR
|
|
128
|
+
GROUP BY StageName
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// ❌ Never SOQL in loop
|
|
132
|
+
for (Account acc : accounts) {
|
|
133
|
+
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id]; // WRONG
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- Selective WHERE clauses: always filter on indexed fields (Id, Name, ExternalId__c, lookup fields)
|
|
138
|
+
- Avoid `SELECT *` — always specify fields explicitly
|
|
139
|
+
- Use `LIMIT` on unbounded queries
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## LWC — Core Rules
|
|
144
|
+
|
|
145
|
+
### File Structure
|
|
146
|
+
```
|
|
147
|
+
force-app/main/default/lwc/
|
|
148
|
+
└── myComponent/
|
|
149
|
+
├── myComponent.html
|
|
150
|
+
├── myComponent.js
|
|
151
|
+
├── myComponent.css
|
|
152
|
+
└── myComponent.js-meta.xml
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Naming
|
|
156
|
+
- Component folder: **camelCase** (`accountCard`, `opportunityList`)
|
|
157
|
+
- Never PascalCase for LWC folders
|
|
158
|
+
|
|
159
|
+
### Brand Theme — MANDATORY `:host` block
|
|
160
|
+
Every LWC CSS file must include this exact token block:
|
|
161
|
+
|
|
162
|
+
```css
|
|
163
|
+
:host {
|
|
164
|
+
/* ── Brand ── */
|
|
165
|
+
--brand: #f37e20;
|
|
166
|
+
--brand-hover: #d86a13;
|
|
167
|
+
--brand-light: #ff9f4d;
|
|
168
|
+
--brand-dim: rgba(243, 126, 32, 0.08);
|
|
169
|
+
--brand-border: rgba(243, 126, 32, 0.22);
|
|
170
|
+
--brand-shadow: rgba(243, 126, 32, 0.2);
|
|
171
|
+
|
|
172
|
+
/* ── User Accent ── */
|
|
173
|
+
--user-color: #0070d2;
|
|
174
|
+
|
|
175
|
+
/* ── Text ── */
|
|
176
|
+
--text-primary: #1d1c1d;
|
|
177
|
+
--text-secondary: #616061;
|
|
178
|
+
--text-muted: #9b9a9b;
|
|
179
|
+
|
|
180
|
+
/* ── Surface ── */
|
|
181
|
+
--surface: #ffffff;
|
|
182
|
+
--surface-dim: #f8f8f8;
|
|
183
|
+
--border: #e8e8e8;
|
|
184
|
+
--border-light: #f0f0f0;
|
|
185
|
+
|
|
186
|
+
/* ── Typography ── */
|
|
187
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
188
|
+
--mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
189
|
+
|
|
190
|
+
/* ── Radius ── */
|
|
191
|
+
--r-sm: 4px;
|
|
192
|
+
--r-md: 8px;
|
|
193
|
+
--r-lg: 12px;
|
|
194
|
+
--r-xl: 18px;
|
|
195
|
+
--r-pill: 999px;
|
|
196
|
+
|
|
197
|
+
/* ── Transition ── */
|
|
198
|
+
--t: 0.15s ease;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Token rules:**
|
|
203
|
+
- ❌ Never hardcode `#f37e20` — always `var(--brand)`
|
|
204
|
+
- ❌ Never use `var(--lwc-colorTextDefault)` for brand colors
|
|
205
|
+
- ✅ Border-radius: always `var(--r-sm/md/lg/xl/r-pill)`
|
|
206
|
+
- ✅ Font: `var(--font)` for UI text, `var(--mono)` for code/IDs
|
|
207
|
+
- ✅ Transitions: `var(--t)` for hover/active
|
|
208
|
+
|
|
209
|
+
### Version Badge — MANDATORY on every LWC
|
|
210
|
+
Every LWC must display a version badge in its header/initiate screen.
|
|
211
|
+
|
|
212
|
+
**Format:** `vMAJOR.MINOR.PATCH+SEASON'YEAR` → e.g. `v2.1.0+S'26`
|
|
213
|
+
- SEASON codes: `S` = Summer, `Sp` = Spring, `W` = Winter
|
|
214
|
+
- YEAR: last 2 digits of Salesforce release year
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<!-- HTML pattern -->
|
|
218
|
+
<span class="myComponent-version-badge">v1.0.0<span class="badge-season">+S'26</span></span>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```css
|
|
222
|
+
/* CSS pattern */
|
|
223
|
+
.myComponent-version-badge {
|
|
224
|
+
display: inline-block;
|
|
225
|
+
font-size: 9px;
|
|
226
|
+
font-weight: 700;
|
|
227
|
+
color: var(--brand);
|
|
228
|
+
background: var(--brand-dim);
|
|
229
|
+
border: 1px solid var(--brand-border);
|
|
230
|
+
border-radius: var(--r-pill);
|
|
231
|
+
padding: 1px 7px;
|
|
232
|
+
letter-spacing: 0.5px;
|
|
233
|
+
text-transform: uppercase;
|
|
234
|
+
}
|
|
235
|
+
.myComponent-version-badge .badge-season {
|
|
236
|
+
margin-left: 2px;
|
|
237
|
+
font-weight: 800;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Version increment rules:**
|
|
242
|
+
- New feature in a season → bump MINOR, reset PATCH to 0
|
|
243
|
+
- Bugfix same season → bump PATCH only
|
|
244
|
+
- Next Salesforce season → update SEASON+YEAR, evaluate MINOR bump
|
|
245
|
+
|
|
246
|
+
### JS Best Practices
|
|
247
|
+
```javascript
|
|
248
|
+
import { LightningElement, api, wire, track } from 'lwc';
|
|
249
|
+
|
|
250
|
+
export default class MyComponent extends LightningElement {
|
|
251
|
+
// @api for public properties (parent → child)
|
|
252
|
+
@api recordId;
|
|
253
|
+
|
|
254
|
+
// @track only needed for nested object/array mutation detection
|
|
255
|
+
// primitives are reactive by default
|
|
256
|
+
|
|
257
|
+
// ❌ Never use @wire adapter inside a loop
|
|
258
|
+
// ✅ Wire at component level, filter in getter
|
|
259
|
+
@wire(getRelatedItems, { recordId: '$recordId' })
|
|
260
|
+
wiredItems;
|
|
261
|
+
|
|
262
|
+
get processedItems() {
|
|
263
|
+
return this.wiredItems.data?.filter(i => i.Active__c) ?? [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- JSDoc required on all public methods and `@api` properties
|
|
269
|
+
- Use optional chaining (`?.`) and nullish coalescing (`??`) for safety
|
|
270
|
+
- Fire events upward with `CustomEvent`, never call parent methods directly
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Integration Patterns
|
|
275
|
+
|
|
276
|
+
### REST Callout
|
|
277
|
+
```apex
|
|
278
|
+
HttpRequest req = new HttpRequest();
|
|
279
|
+
req.setEndpoint('callout:My_Named_Credential/api/endpoint');
|
|
280
|
+
req.setMethod('GET');
|
|
281
|
+
req.setTimeout(10000);
|
|
282
|
+
|
|
283
|
+
HttpResponse res = new Http().send(req);
|
|
284
|
+
if (res.getStatusCode() != 200) {
|
|
285
|
+
throw new CalloutException('HTTP ' + res.getStatusCode() + ': ' + res.getBody());
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
- Always use Named Credentials — never hardcode URLs or tokens
|
|
289
|
+
- Set timeout explicitly
|
|
290
|
+
- Throw `CalloutException` in helper; let `@AuraEnabled` caller wrap as `AuraHandledException`
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## SF CLI — Common Dev Commands
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
# Pull metadata from org
|
|
298
|
+
sf project retrieve start --metadata ApexClass:MyClass --target-org <alias>
|
|
299
|
+
|
|
300
|
+
# Push local source to scratch org
|
|
301
|
+
sf project deploy start --target-org <alias>
|
|
302
|
+
|
|
303
|
+
# Run specific test
|
|
304
|
+
sf apex run test --class-names MyClassTest --target-org <alias> --result-format human
|
|
305
|
+
|
|
306
|
+
# Run all tests
|
|
307
|
+
sf apex run test --test-level RunAllTestsInOrg --target-org <alias>
|
|
308
|
+
|
|
309
|
+
# Open org in browser
|
|
310
|
+
sf org open --target-org <alias>
|
|
311
|
+
|
|
312
|
+
# Execute anonymous Apex
|
|
313
|
+
sf apex run --target-org <alias> --file scripts/apex/myScript.apex
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Code Review Checklist (before every PR)
|
|
319
|
+
- [ ] No SOQL/DML inside loops
|
|
320
|
+
- [ ] Test coverage ≥ 85%
|
|
321
|
+
- [ ] No `@wire` in loops (LWC)
|
|
322
|
+
- [ ] All public methods have ApexDoc/JSDoc
|
|
323
|
+
- [ ] No hardcoded IDs, credentials, or org-specific values
|
|
324
|
+
- [ ] `with sharing` declared on all Apex classes
|
|
325
|
+
- [ ] CRUD check present where applicable (using `Schema.getGlobalDescribe()`)
|
|
326
|
+
- [ ] All `@AuraEnabled` methods wrapped in try-catch → `AuraHandledException`
|
|
327
|
+
- [ ] LWC has `:host` brand token block
|
|
328
|
+
- [ ] LWC has version badge
|