sf-forcekit 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 +106 -0
- package/bin/cli.js +85 -0
- package/package.json +44 -0
- package/templates/ai-dir/README.md +155 -0
- package/templates/ai-dir/agentforce.md +213 -0
- package/templates/ai-dir/architecture.md +123 -0
- package/templates/ai-dir/commands.md +276 -0
- package/templates/ai-dir/context-snapshots/TEMPLATE.md +64 -0
- package/templates/ai-dir/conventions.md +242 -0
- package/templates/ai-dir/current-state.md +113 -0
- package/templates/ai-dir/debugging-notes.md +165 -0
- package/templates/ai-dir/deployment.md +161 -0
- package/templates/ai-dir/integrations.md +199 -0
- package/templates/ai-dir/inventory.md +209 -0
- package/templates/ai-dir/known-issues.md +124 -0
- package/templates/ai-dir/org-context.md +110 -0
- package/templates/ai-dir/performance.md +312 -0
- package/templates/ai-dir/prompts/agentforce.md +163 -0
- package/templates/ai-dir/prompts/apex.md +165 -0
- package/templates/ai-dir/prompts/flows.md +125 -0
- package/templates/ai-dir/prompts/lwc.md +230 -0
- package/templates/ai-dir/prompts/security.md +181 -0
- package/templates/ai-dir/prompts/testing.md +269 -0
- package/templates/ai-dir/rules.md +238 -0
- package/templates/ai-dir/scripts/update_state.py +1406 -0
- package/templates/ai-dir/source-of-truth.md +180 -0
- package/templates/ai-dir/templates/Selector.cls +113 -0
- package/templates/ai-dir/templates/Service.cls +132 -0
- package/templates/ai-dir/templates/TestClass.cls +143 -0
- package/templates/ai-dir/templates/TriggerHandler.cls +67 -0
- package/templates/ai-dir/testing-strategy.md +342 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Source of Truth
|
|
2
|
+
|
|
3
|
+
> 🚫 **NEVER GUESS. ALWAYS VERIFY.**
|
|
4
|
+
> This file tells agents WHERE to look things up instead of hallucinating answers.
|
|
5
|
+
> If you can't verify something, say "I'm not sure — please verify" instead of making it up.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Golden Rule
|
|
10
|
+
|
|
11
|
+
> [!CAUTION]
|
|
12
|
+
> **If you are not 100% certain something exists (a field, object, class, method, CLI flag, API endpoint), you MUST verify it using the methods below. Making up Salesforce metadata is the #1 source of broken code.**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## How to Verify: Metadata
|
|
17
|
+
|
|
18
|
+
### Objects & Fields — Run SOQL
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Check if an object exists
|
|
22
|
+
sf data query --query "SELECT QualifiedApiName FROM EntityDefinition WHERE QualifiedApiName = 'Account'" \
|
|
23
|
+
--target-org <target_org> --use-tooling-api
|
|
24
|
+
|
|
25
|
+
# Check if a field exists on an object
|
|
26
|
+
sf data query --query "SELECT QualifiedApiName, DataType FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = 'Account' AND QualifiedApiName = 'Industry'" \
|
|
27
|
+
--target-org <target_org> --use-tooling-api
|
|
28
|
+
|
|
29
|
+
# List ALL custom fields on an object
|
|
30
|
+
sf data query --query "SELECT QualifiedApiName, DataType FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = 'MyObject__c'" \
|
|
31
|
+
--target-org <target_org> --use-tooling-api
|
|
32
|
+
|
|
33
|
+
# List ALL custom objects in the org
|
|
34
|
+
sf data query --query "SELECT QualifiedApiName FROM EntityDefinition WHERE QualifiedApiName LIKE '%__c'" \
|
|
35
|
+
--target-org <target_org> --use-tooling-api
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Apex Classes — Check Tooling API
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Check if an Apex class exists
|
|
42
|
+
sf data query --query "SELECT Name, Status FROM ApexClass WHERE Name = 'AccountService'" \
|
|
43
|
+
--target-org <target_org> --use-tooling-api
|
|
44
|
+
|
|
45
|
+
# List all Apex classes
|
|
46
|
+
sf data query --query "SELECT Name, Status, ApiVersion FROM ApexClass ORDER BY Name" \
|
|
47
|
+
--target-org <target_org> --use-tooling-api
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Flows — Check Tooling API
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# List active flows
|
|
54
|
+
sf data query --query "SELECT DeveloperName, ProcessType, Status FROM FlowDefinitionView WHERE IsActive = true" \
|
|
55
|
+
--target-org <target_org> --use-tooling-api
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Custom Labels
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sf data query --query "SELECT Name, Value FROM ExternalString" \
|
|
62
|
+
--target-org <target_org> --use-tooling-api
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Named Credentials
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
sf data query --query "SELECT DeveloperName, Endpoint FROM NamedCredential" \
|
|
69
|
+
--target-org <target_org> --use-tooling-api
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Custom Metadata Types
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# List CMDT types
|
|
76
|
+
sf data query --query "SELECT QualifiedApiName FROM EntityDefinition WHERE QualifiedApiName LIKE '%__mdt'" \
|
|
77
|
+
--target-org <target_org> --use-tooling-api
|
|
78
|
+
|
|
79
|
+
# Query CMDT records
|
|
80
|
+
sf data query --query "SELECT DeveloperName, Label FROM MyConfig__mdt" \
|
|
81
|
+
--target-org <target_org>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## How to Verify: Local Project Files
|
|
87
|
+
|
|
88
|
+
Before referencing a class, trigger, or component — check if it exists in the project:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Check if an Apex class exists locally
|
|
92
|
+
ls force-app/main/default/classes/AccountService.cls
|
|
93
|
+
|
|
94
|
+
# Check if a trigger exists
|
|
95
|
+
ls force-app/main/default/triggers/AccountTrigger.trigger
|
|
96
|
+
|
|
97
|
+
# Check if an LWC exists
|
|
98
|
+
ls force-app/main/default/lwc/accountSummary/
|
|
99
|
+
|
|
100
|
+
# Search for a field reference across the project
|
|
101
|
+
grep -r "MyField__c" force-app/
|
|
102
|
+
|
|
103
|
+
# Find all classes that reference a specific object
|
|
104
|
+
grep -r "MyObject__c" force-app/main/default/classes/
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## How to Verify: CLI Commands & Flags
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Don't guess CLI flags — check them
|
|
113
|
+
sf project deploy start --help
|
|
114
|
+
sf data query --help
|
|
115
|
+
sf apex run test --help
|
|
116
|
+
sf org open --help
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## How to Verify: Apex Methods & Classes
|
|
122
|
+
|
|
123
|
+
### Standard Library — Trust These
|
|
124
|
+
|
|
125
|
+
These Apex classes/methods are part of the platform and always exist:
|
|
126
|
+
|
|
127
|
+
| Class | Common Methods | Safe to Use |
|
|
128
|
+
|-------|---------------|-------------|
|
|
129
|
+
| `Database` | `insert()`, `update()`, `query()`, `setSavepoint()`, `rollback()` | ✅ |
|
|
130
|
+
| `System` | `debug()`, `assertEquals()`, `runAs()`, `enqueueJob()` | ✅ |
|
|
131
|
+
| `Limits` | `getQueries()`, `getDmlStatements()`, `getCpuTime()`, `getHeapSize()` | ✅ |
|
|
132
|
+
| `Security` | `stripInaccessible()` | ✅ v48.0+ |
|
|
133
|
+
| `JSON` | `serialize()`, `deserialize()`, `deserializeUntyped()` | ✅ |
|
|
134
|
+
| `String` | `isBlank()`, `escapeSingleQuotes()`, `substringAfter()` | ✅ |
|
|
135
|
+
| `Http` / `HttpRequest` / `HttpResponse` | `send()`, `setEndpoint()`, `setMethod()` | ✅ |
|
|
136
|
+
| `Test` | `startTest()`, `stopTest()`, `setMock()`, `isRunningTest()` | ✅ |
|
|
137
|
+
| `EventBus` | `publish()` | ✅ |
|
|
138
|
+
| `UserInfo` | `getUserId()`, `getUserName()`, `getOrganizationId()` | ✅ |
|
|
139
|
+
| `Schema` | `getGlobalDescribe()`, `describeSObjects()` | ✅ |
|
|
140
|
+
|
|
141
|
+
### Custom Classes — ALWAYS VERIFY
|
|
142
|
+
|
|
143
|
+
Never assume a custom class (`*Service`, `*Selector`, `*Handler`, `Logger`, `TestDataFactory`) exists.
|
|
144
|
+
Check the local project files or `inventory.md` first.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Common Hallucination Traps
|
|
149
|
+
|
|
150
|
+
| Trap | What Agents Get Wrong | How to Avoid |
|
|
151
|
+
|------|----------------------|-------------|
|
|
152
|
+
| **Field names** | Inventing `Account.HealthScore__c` that doesn't exist | Query `FieldDefinition` via Tooling API |
|
|
153
|
+
| **Object names** | Using `OrderItem` vs `OrderProduct` vs `OpportunityLineItem` | Query `EntityDefinition` via Tooling API |
|
|
154
|
+
| **API methods** | Making up `Database.upsertImmediate()` (doesn't exist) | Only use methods from the standard library table above |
|
|
155
|
+
| **CLI flags** | Inventing `--run-all-tests` (actual: `--test-level RunAllTestsInOrg`) | Run `--help` on the command first |
|
|
156
|
+
| **Sharing keywords** | Using `inherited sharing` incorrectly | Only: `with sharing`, `without sharing`, `inherited sharing` |
|
|
157
|
+
| **SOQL keywords** | Using `WITH SYSTEM_MODE` in wrong context | Only `WITH USER_MODE` or `WITH SECURITY_ENFORCED` in SOQL |
|
|
158
|
+
| **Governor limits** | Wrong numbers (e.g., "200 SOQL queries per transaction") | Reference `org-context.md` — it's 100 |
|
|
159
|
+
| **Trigger events** | Using `before undelete` (doesn't exist) | Valid: before/after insert/update/delete, after undelete |
|
|
160
|
+
| **LWC decorators** | Making up `@track` behavior (auto-reactive since v40) | Check current LWC docs |
|
|
161
|
+
| **Flow types** | Referencing "Before-Delete Flow" (doesn't exist natively) | Before-Save (insert/update only), After-Save, Scheduled, etc. |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Confidence Signals
|
|
166
|
+
|
|
167
|
+
When you're not certain about something, use these prefixes in your response:
|
|
168
|
+
|
|
169
|
+
| Prefix | Meaning |
|
|
170
|
+
|--------|---------|
|
|
171
|
+
| ✅ **Verified** | Confirmed via CLI query, file check, or documentation |
|
|
172
|
+
| ⚠️ **Assumed** | Reasonable assumption but not verified — flag for human review |
|
|
173
|
+
| ❓ **Uncertain** | Not sure — explicitly ask the developer to confirm |
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
```
|
|
177
|
+
✅ Verified: Account.Industry field exists (standard field)
|
|
178
|
+
⚠️ Assumed: Account.HealthScore__c exists based on architecture.md — please confirm
|
|
179
|
+
❓ Uncertain: Not sure if the org has Platform Events enabled — check org-context.md
|
|
180
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Selector class for {Object} SOQL queries.
|
|
3
|
+
* ALL SOQL for {Object} lives here — nowhere else.
|
|
4
|
+
* Called by {Object}Service and controllers.
|
|
5
|
+
*
|
|
6
|
+
* Architecture: Trigger → Handler → Service → SELECTOR
|
|
7
|
+
*
|
|
8
|
+
* @author [Your Name]
|
|
9
|
+
* @date [Date]
|
|
10
|
+
*/
|
|
11
|
+
public with sharing class {Object}Selector {
|
|
12
|
+
|
|
13
|
+
// ─── Field Sets ─────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @description Default fields for standard queries.
|
|
17
|
+
* Update this list when adding new fields to queries.
|
|
18
|
+
*/
|
|
19
|
+
private static final String DEFAULT_FIELDS =
|
|
20
|
+
'Id, Name, Status__c, OwnerId, CreatedDate, LastModifiedDate';
|
|
21
|
+
|
|
22
|
+
// ─── Query Methods ──────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @description Gets records by their Ids.
|
|
26
|
+
* @param ids Set of record Ids to retrieve.
|
|
27
|
+
* @return List of records with default fields.
|
|
28
|
+
*/
|
|
29
|
+
public static List<{Object__c}> getByIds(Set<Id> ids) {
|
|
30
|
+
return [
|
|
31
|
+
SELECT Id, Name, Status__c, OwnerId, CreatedDate
|
|
32
|
+
FROM {Object__c}
|
|
33
|
+
WHERE Id IN :ids
|
|
34
|
+
WITH USER_MODE
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @description Gets records by related Account Ids.
|
|
40
|
+
* @param accountIds Set of Account Ids.
|
|
41
|
+
* @return List of records ordered by CreatedDate descending.
|
|
42
|
+
*/
|
|
43
|
+
public static List<{Object__c}> getByAccountIds(Set<Id> accountIds) {
|
|
44
|
+
return [
|
|
45
|
+
SELECT Id, Name, Status__c, Account__c, Account__r.Name
|
|
46
|
+
FROM {Object__c}
|
|
47
|
+
WHERE Account__c IN :accountIds
|
|
48
|
+
WITH USER_MODE
|
|
49
|
+
ORDER BY CreatedDate DESC
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @description Gets records by status.
|
|
55
|
+
* @param statuses Set of status values to filter by.
|
|
56
|
+
* @return List of matching records.
|
|
57
|
+
*/
|
|
58
|
+
public static List<{Object__c}> getByStatus(Set<String> statuses) {
|
|
59
|
+
return [
|
|
60
|
+
SELECT Id, Name, Status__c, OwnerId
|
|
61
|
+
FROM {Object__c}
|
|
62
|
+
WHERE Status__c IN :statuses
|
|
63
|
+
WITH USER_MODE
|
|
64
|
+
ORDER BY LastModifiedDate DESC
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @description Gets records with child relationships.
|
|
70
|
+
* @param ids Set of record Ids.
|
|
71
|
+
* @return List of records with child records.
|
|
72
|
+
*/
|
|
73
|
+
public static List<{Object__c}> getWithChildren(Set<Id> ids) {
|
|
74
|
+
return [
|
|
75
|
+
SELECT Id, Name, Status__c,
|
|
76
|
+
(SELECT Id, Name FROM ChildObjects__r ORDER BY CreatedDate)
|
|
77
|
+
FROM {Object__c}
|
|
78
|
+
WHERE Id IN :ids
|
|
79
|
+
WITH USER_MODE
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @description Counts records by status (aggregate query).
|
|
85
|
+
* @return List of AggregateResult with Status__c and record count.
|
|
86
|
+
*/
|
|
87
|
+
public static List<AggregateResult> countByStatus() {
|
|
88
|
+
return [
|
|
89
|
+
SELECT Status__c, COUNT(Id) recordCount
|
|
90
|
+
FROM {Object__c}
|
|
91
|
+
WITH USER_MODE
|
|
92
|
+
GROUP BY Status__c
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @description Searches records by name (SOSL alternative using LIKE).
|
|
98
|
+
* @param searchTerm The term to search for.
|
|
99
|
+
* @param limitSize Maximum results to return.
|
|
100
|
+
* @return List of matching records.
|
|
101
|
+
*/
|
|
102
|
+
public static List<{Object__c}> searchByName(String searchTerm, Integer limitSize) {
|
|
103
|
+
String safeTerm = '%' + String.escapeSingleQuotes(searchTerm) + '%';
|
|
104
|
+
return [
|
|
105
|
+
SELECT Id, Name, Status__c
|
|
106
|
+
FROM {Object__c}
|
|
107
|
+
WHERE Name LIKE :safeTerm
|
|
108
|
+
WITH USER_MODE
|
|
109
|
+
ORDER BY Name
|
|
110
|
+
LIMIT :limitSize
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Service class for {Object} business logic.
|
|
3
|
+
* All business logic for {Object} lives here.
|
|
4
|
+
* Called by {Object}TriggerHandler and controllers.
|
|
5
|
+
* Calls {Object}Selector for data access.
|
|
6
|
+
*
|
|
7
|
+
* Architecture: Trigger → Handler → SERVICE → Selector
|
|
8
|
+
*
|
|
9
|
+
* @author [Your Name]
|
|
10
|
+
* @date [Date]
|
|
11
|
+
*/
|
|
12
|
+
public with sharing class {Object}Service {
|
|
13
|
+
|
|
14
|
+
// ─── Custom Exception ───────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
public class {Object}ServiceException extends Exception {}
|
|
17
|
+
|
|
18
|
+
// ─── Trigger Context Methods ────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @description Handles before insert logic.
|
|
22
|
+
* @param newRecords List of new records being inserted.
|
|
23
|
+
*/
|
|
24
|
+
public static void onBeforeInsert(List<{Object__c}> newRecords) {
|
|
25
|
+
setDefaults(newRecords);
|
|
26
|
+
validate(newRecords);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @description Handles after insert logic.
|
|
31
|
+
* @param newRecords List of newly inserted records.
|
|
32
|
+
*/
|
|
33
|
+
public static void onAfterInsert(List<{Object__c}> newRecords) {
|
|
34
|
+
// Example: Create related records, publish events
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @description Handles before update logic.
|
|
39
|
+
* @param newRecords List of records with new values.
|
|
40
|
+
* @param oldMap Map of records with previous values.
|
|
41
|
+
*/
|
|
42
|
+
public static void onBeforeUpdate(
|
|
43
|
+
List<{Object__c}> newRecords,
|
|
44
|
+
Map<Id, {Object__c}> oldMap
|
|
45
|
+
) {
|
|
46
|
+
List<{Object__c}> statusChanged = filterStatusChanged(newRecords, oldMap);
|
|
47
|
+
if (!statusChanged.isEmpty()) {
|
|
48
|
+
validateStatusTransition(statusChanged, oldMap);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @description Handles after update logic.
|
|
54
|
+
* @param newRecords List of records with new values.
|
|
55
|
+
* @param oldMap Map of records with previous values.
|
|
56
|
+
*/
|
|
57
|
+
public static void onAfterUpdate(
|
|
58
|
+
List<{Object__c}> newRecords,
|
|
59
|
+
Map<Id, {Object__c}> oldMap
|
|
60
|
+
) {
|
|
61
|
+
List<{Object__c}> statusChanged = filterStatusChanged(newRecords, oldMap);
|
|
62
|
+
if (!statusChanged.isEmpty()) {
|
|
63
|
+
processStatusChange(statusChanged, oldMap);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Public Service Methods ─────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @description Creates {Object} records from DTOs.
|
|
71
|
+
* @param dtos List of data transfer objects.
|
|
72
|
+
* @return List of created records with Ids.
|
|
73
|
+
*/
|
|
74
|
+
public static List<{Object__c}> createFromDTOs(List<{Object}DTO> dtos) {
|
|
75
|
+
List<{Object__c}> records = new List<{Object__c}>();
|
|
76
|
+
for ({Object}DTO dto : dtos) {
|
|
77
|
+
records.add(dto.toSObject());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
SObjectAccessDecision decision = Security.stripInaccessible(
|
|
81
|
+
AccessType.CREATABLE, records
|
|
82
|
+
);
|
|
83
|
+
insert decision.getRecords();
|
|
84
|
+
return decision.getRecords();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Private Helper Methods ─────────────────────────────────
|
|
88
|
+
|
|
89
|
+
private static void setDefaults(List<{Object__c}> records) {
|
|
90
|
+
for ({Object__c} record : records) {
|
|
91
|
+
if (record.Status__c == null) {
|
|
92
|
+
record.Status__c = 'New';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static void validate(List<{Object__c}> records) {
|
|
98
|
+
for ({Object__c} record : records) {
|
|
99
|
+
if (String.isBlank(record.Name)) {
|
|
100
|
+
record.Name.addError('Name is required.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static List<{Object__c}> filterStatusChanged(
|
|
106
|
+
List<{Object__c}> newRecords,
|
|
107
|
+
Map<Id, {Object__c}> oldMap
|
|
108
|
+
) {
|
|
109
|
+
List<{Object__c}> changed = new List<{Object__c}>();
|
|
110
|
+
for ({Object__c} record : newRecords) {
|
|
111
|
+
{Object__c} oldRecord = oldMap.get(record.Id);
|
|
112
|
+
if (record.Status__c != oldRecord.Status__c) {
|
|
113
|
+
changed.add(record);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return changed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private static void validateStatusTransition(
|
|
120
|
+
List<{Object__c}> records,
|
|
121
|
+
Map<Id, {Object__c}> oldMap
|
|
122
|
+
) {
|
|
123
|
+
// Validate allowed status transitions
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private static void processStatusChange(
|
|
127
|
+
List<{Object__c}> records,
|
|
128
|
+
Map<Id, {Object__c}> oldMap
|
|
129
|
+
) {
|
|
130
|
+
// Process downstream effects of status change
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Test class for {ClassName}.
|
|
3
|
+
* Tests cover: bulk (200+), positive, negative, and permission scenarios.
|
|
4
|
+
*
|
|
5
|
+
* @author [Your Name]
|
|
6
|
+
* @date [Date]
|
|
7
|
+
*/
|
|
8
|
+
@IsTest
|
|
9
|
+
private class {ClassName}Test {
|
|
10
|
+
|
|
11
|
+
// ─── Test Data Setup ────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
@TestSetup
|
|
14
|
+
static void setupTestData() {
|
|
15
|
+
// Create bulk test data (200+ records)
|
|
16
|
+
List<Account> accounts = TestDataFactory.createAccounts(200);
|
|
17
|
+
insert accounts;
|
|
18
|
+
|
|
19
|
+
// Create related records if needed
|
|
20
|
+
// List<Contact> contacts = TestDataFactory.createContacts(accounts, 2);
|
|
21
|
+
// insert contacts;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Positive Tests ─────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
@IsTest
|
|
27
|
+
static void testMethod_PositiveScenario() {
|
|
28
|
+
// Arrange
|
|
29
|
+
List<Account> accounts = [SELECT Id, Name FROM Account];
|
|
30
|
+
System.assertEquals(200, accounts.size(), 'TestSetup should create 200 accounts');
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
Test.startTest();
|
|
34
|
+
// Call the method under test
|
|
35
|
+
Test.stopTest();
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
// System.assertEquals(expected, actual, 'Descriptive message');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Bulk Tests ─────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@IsTest
|
|
44
|
+
static void testMethod_Bulk200Records() {
|
|
45
|
+
// Arrange
|
|
46
|
+
List<Account> accounts = [SELECT Id FROM Account];
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
Test.startTest();
|
|
50
|
+
// Process all 200 records
|
|
51
|
+
Test.stopTest();
|
|
52
|
+
|
|
53
|
+
// Assert — no governor limit exceptions
|
|
54
|
+
// Verify all records processed correctly
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Negative Tests ─────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
@IsTest
|
|
60
|
+
static void testMethod_NullInput() {
|
|
61
|
+
Boolean exceptionThrown = false;
|
|
62
|
+
|
|
63
|
+
Test.startTest();
|
|
64
|
+
try {
|
|
65
|
+
// Call method with null/invalid input
|
|
66
|
+
} catch (Exception e) {
|
|
67
|
+
exceptionThrown = true;
|
|
68
|
+
System.assert(
|
|
69
|
+
e.getMessage().contains('expected error text'),
|
|
70
|
+
'Exception should contain meaningful message: ' + e.getMessage()
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
Test.stopTest();
|
|
74
|
+
|
|
75
|
+
System.assert(exceptionThrown, 'Should throw exception for null input');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@IsTest
|
|
79
|
+
static void testMethod_InvalidData() {
|
|
80
|
+
// Arrange — create invalid record
|
|
81
|
+
|
|
82
|
+
// Act & Assert
|
|
83
|
+
Test.startTest();
|
|
84
|
+
try {
|
|
85
|
+
// Attempt invalid operation
|
|
86
|
+
System.assert(false, 'Should have thrown an exception');
|
|
87
|
+
} catch (DmlException e) {
|
|
88
|
+
System.assert(
|
|
89
|
+
e.getMessage().contains('REQUIRED_FIELD_MISSING')
|
|
90
|
+
|| e.getMessage().contains('FIELD_CUSTOM_VALIDATION_EXCEPTION'),
|
|
91
|
+
'Should fail validation: ' + e.getMessage()
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
Test.stopTest();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Permission Tests ───────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
@IsTest
|
|
100
|
+
static void testMethod_RestrictedUser() {
|
|
101
|
+
User restrictedUser = TestDataFactory.createStandardUser();
|
|
102
|
+
insert restrictedUser;
|
|
103
|
+
|
|
104
|
+
System.runAs(restrictedUser) {
|
|
105
|
+
Test.startTest();
|
|
106
|
+
// Test with restricted permissions
|
|
107
|
+
// Verify FLS/CRUD enforcement
|
|
108
|
+
Test.stopTest();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Callout Tests (if applicable) ──────────────────────────
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
@IsTest
|
|
116
|
+
static void testCallout_Success() {
|
|
117
|
+
Test.setMock(HttpCalloutMock.class, new MockSuccessResponse());
|
|
118
|
+
|
|
119
|
+
Test.startTest();
|
|
120
|
+
// Call method that makes callout
|
|
121
|
+
Test.stopTest();
|
|
122
|
+
|
|
123
|
+
// Assert response handled correctly
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@IsTest
|
|
127
|
+
static void testCallout_Failure() {
|
|
128
|
+
Test.setMock(HttpCalloutMock.class, new MockFailureResponse());
|
|
129
|
+
|
|
130
|
+
Test.startTest();
|
|
131
|
+
try {
|
|
132
|
+
// Call method that makes callout
|
|
133
|
+
} catch (IntegrationException e) {
|
|
134
|
+
System.assert(true, 'Should handle callout failure gracefully');
|
|
135
|
+
}
|
|
136
|
+
Test.stopTest();
|
|
137
|
+
}
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
// ─── Helper Methods ─────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
// Add test-specific helpers here
|
|
143
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Base class for trigger handlers.
|
|
3
|
+
* Extend this class for each object's trigger handler.
|
|
4
|
+
* Delegates trigger context to virtual methods.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* trigger AccountTrigger on Account (...) {
|
|
8
|
+
* new AccountTriggerHandler().run();
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* @author [Your Name]
|
|
12
|
+
* @date [Date]
|
|
13
|
+
*/
|
|
14
|
+
public virtual with sharing class TriggerHandler {
|
|
15
|
+
|
|
16
|
+
// Recursion prevention
|
|
17
|
+
private static Set<String> bypassedHandlers = new Set<String>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @description Main entry point. Routes trigger context to virtual methods.
|
|
21
|
+
*/
|
|
22
|
+
public void run() {
|
|
23
|
+
String handlerName = String.valueOf(this).substring(0, String.valueOf(this).indexOf(':'));
|
|
24
|
+
|
|
25
|
+
if (bypassedHandlers.contains(handlerName)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
switch on Trigger.operationType {
|
|
30
|
+
when BEFORE_INSERT { this.beforeInsert(); }
|
|
31
|
+
when BEFORE_UPDATE { this.beforeUpdate(); }
|
|
32
|
+
when BEFORE_DELETE { this.beforeDelete(); }
|
|
33
|
+
when AFTER_INSERT { this.afterInsert(); }
|
|
34
|
+
when AFTER_UPDATE { this.afterUpdate(); }
|
|
35
|
+
when AFTER_DELETE { this.afterDelete(); }
|
|
36
|
+
when AFTER_UNDELETE { this.afterUndelete(); }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Virtual Methods (Override in Subclass) ─────────────────
|
|
41
|
+
|
|
42
|
+
protected virtual void beforeInsert() {}
|
|
43
|
+
protected virtual void beforeUpdate() {}
|
|
44
|
+
protected virtual void beforeDelete() {}
|
|
45
|
+
protected virtual void afterInsert() {}
|
|
46
|
+
protected virtual void afterUpdate() {}
|
|
47
|
+
protected virtual void afterDelete() {}
|
|
48
|
+
protected virtual void afterUndelete() {}
|
|
49
|
+
|
|
50
|
+
// ─── Bypass Control ─────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
public static void bypass(String handlerName) {
|
|
53
|
+
bypassedHandlers.add(handlerName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public static void clearBypass(String handlerName) {
|
|
57
|
+
bypassedHandlers.remove(handlerName);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public static void clearAllBypasses() {
|
|
61
|
+
bypassedHandlers.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public static Boolean isBypassed(String handlerName) {
|
|
65
|
+
return bypassedHandlers.contains(handlerName);
|
|
66
|
+
}
|
|
67
|
+
}
|