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.
Files changed (32) hide show
  1. package/License +21 -0
  2. package/README.md +106 -0
  3. package/bin/cli.js +85 -0
  4. package/package.json +44 -0
  5. package/templates/ai-dir/README.md +155 -0
  6. package/templates/ai-dir/agentforce.md +213 -0
  7. package/templates/ai-dir/architecture.md +123 -0
  8. package/templates/ai-dir/commands.md +276 -0
  9. package/templates/ai-dir/context-snapshots/TEMPLATE.md +64 -0
  10. package/templates/ai-dir/conventions.md +242 -0
  11. package/templates/ai-dir/current-state.md +113 -0
  12. package/templates/ai-dir/debugging-notes.md +165 -0
  13. package/templates/ai-dir/deployment.md +161 -0
  14. package/templates/ai-dir/integrations.md +199 -0
  15. package/templates/ai-dir/inventory.md +209 -0
  16. package/templates/ai-dir/known-issues.md +124 -0
  17. package/templates/ai-dir/org-context.md +110 -0
  18. package/templates/ai-dir/performance.md +312 -0
  19. package/templates/ai-dir/prompts/agentforce.md +163 -0
  20. package/templates/ai-dir/prompts/apex.md +165 -0
  21. package/templates/ai-dir/prompts/flows.md +125 -0
  22. package/templates/ai-dir/prompts/lwc.md +230 -0
  23. package/templates/ai-dir/prompts/security.md +181 -0
  24. package/templates/ai-dir/prompts/testing.md +269 -0
  25. package/templates/ai-dir/rules.md +238 -0
  26. package/templates/ai-dir/scripts/update_state.py +1406 -0
  27. package/templates/ai-dir/source-of-truth.md +180 -0
  28. package/templates/ai-dir/templates/Selector.cls +113 -0
  29. package/templates/ai-dir/templates/Service.cls +132 -0
  30. package/templates/ai-dir/templates/TestClass.cls +143 -0
  31. package/templates/ai-dir/templates/TriggerHandler.cls +67 -0
  32. 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
+ }