@salesforce/afv-skills 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.
Files changed (47) hide show
  1. package/LICENSE.txt +330 -0
  2. package/README.md +466 -0
  3. package/package.json +23 -0
  4. package/skills/apex-class/SKILL.md +253 -0
  5. package/skills/apex-class/examples/AccountDeduplicationBatch.cls +148 -0
  6. package/skills/apex-class/examples/AccountSelector.cls +193 -0
  7. package/skills/apex-class/examples/AccountService.cls +201 -0
  8. package/skills/apex-class/templates/abstract.cls +128 -0
  9. package/skills/apex-class/templates/batch.cls +125 -0
  10. package/skills/apex-class/templates/domain.cls +102 -0
  11. package/skills/apex-class/templates/dto.cls +108 -0
  12. package/skills/apex-class/templates/exception.cls +51 -0
  13. package/skills/apex-class/templates/interface.cls +25 -0
  14. package/skills/apex-class/templates/queueable.cls +92 -0
  15. package/skills/apex-class/templates/schedulable.cls +75 -0
  16. package/skills/apex-class/templates/selector.cls +92 -0
  17. package/skills/apex-class/templates/service.cls +69 -0
  18. package/skills/apex-class/templates/utility.cls +97 -0
  19. package/skills/apex-test-class/SKILL.md +101 -0
  20. package/skills/apex-test-class/references/assertion-patterns.md +209 -0
  21. package/skills/apex-test-class/references/async-testing.md +276 -0
  22. package/skills/apex-test-class/references/mocking-patterns.md +219 -0
  23. package/skills/apex-test-class/references/test-data-factory.md +176 -0
  24. package/skills/deployment-readiness-check/SKILL.md +257 -0
  25. package/skills/deployment-readiness-check/assets/deployment_checklist.md +286 -0
  26. package/skills/deployment-readiness-check/references/rollback_procedures.md +308 -0
  27. package/skills/deployment-readiness-check/scripts/check_metadata.sh +207 -0
  28. package/skills/salesforce-custom-application/SKILL.md +211 -0
  29. package/skills/salesforce-custom-field/SKILL.md +505 -0
  30. package/skills/salesforce-custom-lightning-type/SKILL.md +157 -0
  31. package/skills/salesforce-custom-object/SKILL.md +238 -0
  32. package/skills/salesforce-custom-tab/SKILL.md +78 -0
  33. package/skills/salesforce-experience-site/SKILL.md +178 -0
  34. package/skills/salesforce-flexipage/SKILL.md +445 -0
  35. package/skills/salesforce-flow/SKILL.md +368 -0
  36. package/skills/salesforce-fragment/SKILL.md +42 -0
  37. package/skills/salesforce-lightning-app-build/SKILL.md +254 -0
  38. package/skills/salesforce-list-view/SKILL.md +216 -0
  39. package/skills/salesforce-validation-rule/SKILL.md +72 -0
  40. package/skills/salesforce-web-app-creating-records/SKILL.md +84 -0
  41. package/skills/salesforce-web-app-feature/SKILL.md +70 -0
  42. package/skills/salesforce-web-app-list-and-create-records/SKILL.md +36 -0
  43. package/skills/salesforce-web-application/SKILL.md +34 -0
  44. package/skills/trigger-refactor-pipeline/SKILL.md +191 -0
  45. package/skills/trigger-refactor-pipeline/assets/test_template.apex +321 -0
  46. package/skills/trigger-refactor-pipeline/references/handler_patterns.md +442 -0
  47. package/skills/trigger-refactor-pipeline/scripts/analyze_trigger.py +258 -0
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: trigger-refactor-pipeline
3
+ description: Refactor Salesforce triggers into handler patterns with automated test generation and deployment. Use when modernizing legacy triggers with DML/SOQL in loops or inconsistent patterns.
4
+ license: Apache-2.0
5
+ compatibility: Requires Salesforce CLI, Python 3.9+
6
+ metadata:
7
+ author: afv-library
8
+ version: "1.0"
9
+ allowed-tools: Bash Read Write
10
+ ---
11
+
12
+ ## When to Use This Skill
13
+
14
+ Use this skill when you need to:
15
+ - Modernize legacy triggers with DML/SOQL operations inside loops
16
+ - Refactor triggers that lack clear separation of concerns
17
+ - Implement bulk-safe patterns in existing trigger code
18
+ - Generate comprehensive test coverage for refactored triggers
19
+
20
+ ## Prerequisites
21
+
22
+ Before starting, ensure you have:
23
+ 1. Salesforce CLI installed and authenticated to your target org
24
+ 2. Python 3.9 or higher installed
25
+ 3. The baseline trigger deployed (see Setup section)
26
+
27
+ ## Setup
28
+
29
+ Deploy the baseline anti-pattern trigger to analyze and refactor:
30
+
31
+ ```apex
32
+ // ❌ Anti-pattern: all logic stuffed into the trigger, with DML/SOQL in loops.
33
+ trigger OpportunityTrigger on Opportunity (before insert, before update, after update) {
34
+ // BEFORE INSERT: validate Closed Won w/ low Amount
35
+ if (Trigger.isBefore && Trigger.isInsert) {
36
+ for (Opportunity o : Trigger.new) {
37
+ if (o.StageName == 'Closed Won' && (o.Amount == null || o.Amount < 1000)) {
38
+ o.addError('Closed Won opportunities must have Amount ≥ 1000.');
39
+ }
40
+ }
41
+ }
42
+
43
+ // BEFORE UPDATE: if Stage changed, overwrite Description
44
+ if (Trigger.isBefore && Trigger.isUpdate) {
45
+ for (Opportunity o : Trigger.new) {
46
+ Opportunity oldO = Trigger.oldMap.get(o.Id);
47
+ if (o.StageName != oldO.StageName) {
48
+ o.Description = 'Stage changed from ' + oldO.StageName + ' to ' + o.StageName;
49
+ }
50
+ }
51
+ }
52
+
53
+ // AFTER UPDATE: when Stage becomes Closed Won, create a follow-up Task
54
+ if (Trigger.isAfter && Trigger.isUpdate) {
55
+ for (Opportunity o : Trigger.new) {
56
+ Opportunity oldO = Trigger.oldMap.get(o.Id);
57
+ if (o.StageName == 'Closed Won' && oldO.StageName != 'Closed Won') {
58
+ Task t = new Task(
59
+ WhatId = o.Id,
60
+ OwnerId = o.OwnerId,
61
+ Subject = 'Send thank-you',
62
+ Status = 'Not Started',
63
+ Priority = 'Normal',
64
+ ActivityDate = Date.today()
65
+ );
66
+ insert t; // ❌ DML in a loop
67
+ }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ Deploy this to your org:
74
+ ```bash
75
+ sf project deploy start --source-dir force-app/main/default/triggers
76
+ ```
77
+
78
+ ## Step 1: Analyze the Trigger
79
+
80
+ Run the analysis script to identify anti-patterns and generate a report:
81
+
82
+ ```bash
83
+ python scripts/analyze_trigger.py OpportunityTrigger
84
+ ```
85
+
86
+ The script will output:
87
+ - **DML in loops** - Line numbers where DML operations occur inside iteration
88
+ - **SOQL in loops** - Line numbers where SOQL queries occur inside iteration
89
+ - **Missing bulkification** - Areas where collection-based processing is needed
90
+ - **Complexity score** - Overall trigger complexity rating (1-10)
91
+ - **Recommended approach** - Suggested handler pattern based on trigger contexts
92
+
93
+ Review the analysis report before proceeding to refactoring.
94
+
95
+ ## Step 2: Review Handler Patterns
96
+
97
+ Consult the [handler patterns reference](references/handler_patterns.md) to understand:
98
+ - **Single-responsibility handlers** - One handler class per trigger context
99
+ - **Unified handler approach** - Single handler with context methods
100
+ - **Bulk collection strategies** - How to aggregate DML/SOQL outside loops
101
+ - **Best practices** - Error handling, test boundaries, deployment order
102
+
103
+ Choose the pattern that best fits your trigger's complexity and team conventions.
104
+
105
+ ## Step 3: Refactor the Trigger
106
+
107
+ Create the handler class using the appropriate pattern from the reference guide:
108
+
109
+ 1. **Extract logic** into handler methods with descriptive names
110
+ 2. **Implement bulk-safe collections** for DML operations
111
+ 3. **Add proper error handling** using try-catch or Database methods
112
+ 4. **Update the trigger** to delegate only, passing Trigger context variables
113
+ 5. **Preserve behavior** - ensure the refactored code produces identical results
114
+
115
+ The trigger should be reduced to simple delegation:
116
+
117
+ ```apex
118
+ trigger OpportunityTrigger on Opportunity (before insert, before update, after update) {
119
+ OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
120
+
121
+ if (Trigger.isBefore && Trigger.isInsert) {
122
+ handler.beforeInsert(Trigger.new);
123
+ }
124
+
125
+ if (Trigger.isBefore && Trigger.isUpdate) {
126
+ handler.beforeUpdate(Trigger.new, Trigger.oldMap);
127
+ }
128
+
129
+ if (Trigger.isAfter && Trigger.isUpdate) {
130
+ handler.afterUpdate(Trigger.new, Trigger.oldMap);
131
+ }
132
+ }
133
+ ```
134
+
135
+ ## Step 4: Generate Tests
136
+
137
+ Use the test template from `assets/test_template.apex` to scaffold your test class:
138
+
139
+ 1. **Copy the template** and rename for your handler
140
+ 2. **Implement setup methods** to create test data
141
+ 3. **Write unit tests** covering each handler method:
142
+ - Positive cases with valid data
143
+ - Negative cases with invalid data
144
+ - Boundary conditions
145
+ 4. **Add bulk tests** with 200+ records to verify bulkification
146
+ 5. **Test mixed scenarios** where only some records qualify for logic
147
+
148
+ Required test coverage:
149
+ - Each handler method must have at least 2 test methods (positive + negative)
150
+ - At least one bulk test with 200+ records
151
+ - Overall code coverage must be 100%
152
+
153
+ ## Step 5: Deploy and Validate
154
+
155
+ Deploy the refactored trigger, handler, and tests:
156
+
157
+ ```bash
158
+ # Deploy all components
159
+ sf project deploy start --source-dir force-app/main/default
160
+
161
+ # Run tests
162
+ sf apex test run --class-names OpportunityTriggerHandlerTest --result-format human --code-coverage
163
+
164
+ # Verify no regressions
165
+ sf apex test run --test-level RunLocalTests --result-format human
166
+ ```
167
+
168
+ Validation checklist:
169
+ - [ ] All new tests pass with 100% coverage
170
+ - [ ] No new governor limit warnings in debug logs
171
+ - [ ] Existing functionality remains unchanged
172
+ - [ ] Deployment to production planned with rollback strategy
173
+
174
+ ## Troubleshooting
175
+
176
+ **Issue**: Tests fail with "System.LimitException: Too many DML statements"
177
+ - **Solution**: Ensure handler methods collect DML operations and execute outside loops
178
+
179
+ **Issue**: Code coverage below 100%
180
+ - **Solution**: Add negative test cases and verify all conditional branches are tested
181
+
182
+ **Issue**: Behavior differs from original trigger
183
+ - **Solution**: Review Trigger context variables (new, old, oldMap) are passed correctly to handler
184
+
185
+ ## Next Steps
186
+
187
+ After successful refactoring:
188
+ 1. Document the new handler pattern in your team's wiki
189
+ 2. Update code review checklist to enforce handler patterns for new triggers
190
+ 3. Identify other legacy triggers for refactoring using this skill
191
+ 4. Consider implementing a trigger framework if managing many triggers
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Test class template for trigger handlers
3
+ *
4
+ * INSTRUCTIONS:
5
+ * 1. Replace [ObjectName] with your SObject (e.g., Opportunity)
6
+ * 2. Replace [HandlerClass] with your handler class name
7
+ * 3. Implement the setupTestData() method with your test records
8
+ * 4. Add specific test methods for each handler method
9
+ * 5. Ensure 100% code coverage
10
+ */
11
+ @IsTest
12
+ private class [ObjectName]TriggerHandlerTest {
13
+
14
+ /**
15
+ * Setup test data that all test methods can use
16
+ */
17
+ @TestSetup
18
+ static void setupTestData() {
19
+ // TODO: Create test records here
20
+ // Example:
21
+ // List<Opportunity> testOpps = new List<Opportunity>();
22
+ // for (Integer i = 0; i < 10; i++) {
23
+ // testOpps.add(new Opportunity(
24
+ // Name = 'Test Opp ' + i,
25
+ // StageName = 'Prospecting',
26
+ // CloseDate = Date.today().addDays(30),
27
+ // Amount = 5000
28
+ // ));
29
+ // }
30
+ // insert testOpps;
31
+ }
32
+
33
+ /**
34
+ * Test beforeInsert handler - Positive case
35
+ */
36
+ @IsTest
37
+ static void testBeforeInsert_Positive() {
38
+ Test.startTest();
39
+
40
+ // TODO: Create valid test records
41
+ // List<Opportunity> testRecords = new List<Opportunity>{
42
+ // new Opportunity(
43
+ // Name = 'Valid Opp',
44
+ // StageName = 'Prospecting',
45
+ // CloseDate = Date.today().addDays(30),
46
+ // Amount = 10000
47
+ // )
48
+ // };
49
+
50
+ // Insert should succeed
51
+ // insert testRecords;
52
+
53
+ Test.stopTest();
54
+
55
+ // TODO: Add assertions
56
+ // List<Opportunity> inserted = [SELECT Id, Name FROM Opportunity WHERE Name = 'Valid Opp'];
57
+ // System.Assert.areEqual(1, inserted.size(), 'Should insert 1 record');
58
+ }
59
+
60
+ /**
61
+ * Test beforeInsert handler - Negative case
62
+ */
63
+ @IsTest
64
+ static void testBeforeInsert_Negative() {
65
+ Test.startTest();
66
+
67
+ Boolean exceptionThrown = false;
68
+
69
+ try {
70
+ // TODO: Create invalid test records that should fail validation
71
+ // List<Opportunity> testRecords = new List<Opportunity>{
72
+ // new Opportunity(
73
+ // Name = 'Invalid Opp',
74
+ // StageName = 'Closed Won',
75
+ // CloseDate = Date.today(),
76
+ // Amount = 500 // Below minimum
77
+ // )
78
+ // };
79
+
80
+ // insert testRecords;
81
+
82
+ } catch (DmlException e) {
83
+ exceptionThrown = true;
84
+ // TODO: Assert error message
85
+ // System.Assert.isTrue(e.getMessage().contains('must have Amount'),
86
+ // 'Should throw validation error');
87
+ }
88
+
89
+ Test.stopTest();
90
+
91
+ // System.Assert.isTrue(exceptionThrown, 'Should have thrown an exception');
92
+ }
93
+
94
+ /**
95
+ * Test beforeUpdate handler - Positive case
96
+ */
97
+ @IsTest
98
+ static void testBeforeUpdate_Positive() {
99
+ // TODO: Query existing test data from @TestSetup
100
+ // List<Opportunity> testOpps = [SELECT Id, StageName, Description FROM Opportunity LIMIT 1];
101
+
102
+ Test.startTest();
103
+
104
+ // TODO: Update records to trigger handler logic
105
+ // testOpps[0].StageName = 'Qualification';
106
+ // update testOpps;
107
+
108
+ Test.stopTest();
109
+
110
+ // TODO: Add assertions
111
+ // Opportunity updated = [SELECT Description FROM Opportunity WHERE Id = :testOpps[0].Id];
112
+ // System.Assert.isTrue(updated.Description.contains('Stage changed'),
113
+ // 'Description should be updated');
114
+ }
115
+
116
+ /**
117
+ * Test beforeUpdate handler - Negative case
118
+ */
119
+ @IsTest
120
+ static void testBeforeUpdate_Negative() {
121
+ // TODO: Implement negative test for beforeUpdate
122
+ Test.startTest();
123
+
124
+ // TODO: Attempt update that should fail
125
+
126
+ Test.stopTest();
127
+
128
+ // TODO: Assert failure occurred
129
+ }
130
+
131
+ /**
132
+ * Test afterInsert handler - Positive case
133
+ */
134
+ @IsTest
135
+ static void testAfterInsert_Positive() {
136
+ Test.startTest();
137
+
138
+ // TODO: Create and insert records
139
+
140
+ Test.stopTest();
141
+
142
+ // TODO: Query for related records created by handler
143
+ // List<Task> createdTasks = [SELECT Id FROM Task];
144
+ // System.Assert.areEqual(1, createdTasks.size(), 'Should create 1 task');
145
+ }
146
+
147
+ /**
148
+ * Test afterUpdate handler - Positive case
149
+ */
150
+ @IsTest
151
+ static void testAfterUpdate_Positive() {
152
+ // TODO: Query existing test data
153
+ // List<Opportunity> testOpps = [SELECT Id, StageName FROM Opportunity LIMIT 1];
154
+
155
+ Test.startTest();
156
+
157
+ // TODO: Update to trigger after-update logic
158
+ // testOpps[0].StageName = 'Closed Won';
159
+ // update testOpps;
160
+
161
+ Test.stopTest();
162
+
163
+ // TODO: Query for side effects (e.g., Tasks created)
164
+ // List<Task> tasks = [SELECT Id, Subject FROM Task WHERE WhatId = :testOpps[0].Id];
165
+ // System.Assert.areEqual(1, tasks.size(), 'Should create 1 task');
166
+ // System.Assert.areEqual('Send thank-you', tasks[0].Subject);
167
+ }
168
+
169
+ /**
170
+ * Test bulk operations - 200+ records
171
+ * Critical for validating bulkification
172
+ */
173
+ @IsTest
174
+ static void testBulkInsert() {
175
+ Test.startTest();
176
+
177
+ // TODO: Create 200+ records
178
+ // List<Opportunity> bulkOpps = new List<Opportunity>();
179
+ // for (Integer i = 0; i < 200; i++) {
180
+ // bulkOpps.add(new Opportunity(
181
+ // Name = 'Bulk Opp ' + i,
182
+ // StageName = 'Prospecting',
183
+ // CloseDate = Date.today().addDays(30),
184
+ // Amount = 10000
185
+ // ));
186
+ // }
187
+
188
+ // insert bulkOpps;
189
+
190
+ Test.stopTest();
191
+
192
+ // TODO: Assert all records inserted successfully
193
+ // List<Opportunity> inserted = [SELECT Id FROM Opportunity WHERE Name LIKE 'Bulk Opp%'];
194
+ // System.Assert.areEqual(200, inserted.size(), 'Should insert all 200 records');
195
+ }
196
+
197
+ /**
198
+ * Test bulk update with mixed scenarios
199
+ * Some records qualify for logic, others don't
200
+ */
201
+ @IsTest
202
+ static void testBulkUpdate_Mixed() {
203
+ // Create test data
204
+ List<Opportunity> testOpps = new List<Opportunity>();
205
+ for (Integer i = 0; i < 50; i++) {
206
+ testOpps.add(new Opportunity(
207
+ Name = 'Bulk Update Opp ' + i,
208
+ StageName = 'Prospecting',
209
+ CloseDate = Date.today().addDays(30),
210
+ Amount = 10000
211
+ ));
212
+ }
213
+ insert testOpps;
214
+
215
+ Test.startTest();
216
+
217
+ // Update half to trigger logic, half to not trigger
218
+ for (Integer i = 0; i < testOpps.size(); i++) {
219
+ if (Math.mod(i, 2) == 0) {
220
+ // TODO: Set condition that triggers handler logic
221
+ // testOpps[i].StageName = 'Closed Won';
222
+ } else {
223
+ // TODO: Set condition that doesn't trigger handler logic
224
+ // testOpps[i].Amount = 15000;
225
+ }
226
+ }
227
+
228
+ // update testOpps;
229
+
230
+ Test.stopTest();
231
+
232
+ // TODO: Assert only qualifying records triggered side effects
233
+ // List<Task> tasks = [SELECT Id FROM Task WHERE WhatId IN :testOpps];
234
+ // System.Assert.areEqual(25, tasks.size(), 'Should create tasks for 25 qualifying records');
235
+ }
236
+
237
+ /**
238
+ * Test governor limits are not exceeded
239
+ */
240
+ @IsTest
241
+ static void testGovernorLimits() {
242
+ Test.startTest();
243
+
244
+ // TODO: Create maximum allowed records
245
+ // List<Opportunity> maxOpps = new List<Opportunity>();
246
+ // for (Integer i = 0; i < 200; i++) {
247
+ // maxOpps.add(new Opportunity(
248
+ // Name = 'Limit Test ' + i,
249
+ // StageName = 'Closed Won',
250
+ // CloseDate = Date.today(),
251
+ // Amount = 10000
252
+ // ));
253
+ // }
254
+
255
+ // insert maxOpps;
256
+
257
+ Test.stopTest();
258
+
259
+ // Assert we're under governor limits
260
+ System.Assert.isTrue(Limits.getDmlStatements() < Limits.getLimitDmlStatements(),
261
+ 'Should not exceed DML statement limit');
262
+ System.Assert.isTrue(Limits.getQueries() < Limits.getLimitQueries(),
263
+ 'Should not exceed SOQL query limit');
264
+ }
265
+
266
+ /**
267
+ * Test with null or empty collections
268
+ * Ensures handler doesn't break with edge cases
269
+ */
270
+ @IsTest
271
+ static void testWithEmptyCollection() {
272
+ Test.startTest();
273
+
274
+ // TODO: Call handler methods with empty lists
275
+ // OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
276
+ // handler.beforeInsert(new List<Opportunity>());
277
+
278
+ Test.stopTest();
279
+
280
+ // If we get here without exception, test passes
281
+ System.Assert.isTrue(true, 'Handler should handle empty collections gracefully');
282
+ }
283
+
284
+ /**
285
+ * Helper method to create test data inline
286
+ * Use when @TestSetup is not sufficient
287
+ */
288
+ private static List<Opportunity> createTestOpportunities(Integer count) {
289
+ List<Opportunity> testOpps = new List<Opportunity>();
290
+
291
+ for (Integer i = 0; i < count; i++) {
292
+ testOpps.add(new Opportunity(
293
+ Name = 'Test Opp ' + i,
294
+ StageName = 'Prospecting',
295
+ CloseDate = Date.today().addDays(30),
296
+ Amount = 10000
297
+ ));
298
+ }
299
+
300
+ return testOpps;
301
+ }
302
+
303
+ /**
304
+ * Helper method to assert task creation
305
+ */
306
+ private static void assertTasksCreated(List<Id> oppIds, Integer expectedCount, String subject) {
307
+ List<Task> tasks = [
308
+ SELECT Id, Subject, WhatId
309
+ FROM Task
310
+ WHERE WhatId IN :oppIds
311
+ ];
312
+
313
+ System.Assert.areEqual(expectedCount, tasks.size(),
314
+ 'Should create ' + expectedCount + ' task(s)');
315
+
316
+ if (expectedCount > 0) {
317
+ System.Assert.areEqual(subject, tasks[0].Subject,
318
+ 'Task subject should match');
319
+ }
320
+ }
321
+ }