├── .circleci
└── config.yml
├── .forceignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── pmd-ruleset.xml
└── server.key.enc
├── config
└── project-scratch-def.json
├── force-app
└── default
│ └── main
│ ├── classes
│ ├── TriggerHandler.cls
│ ├── TriggerHandler.cls-meta.xml
│ ├── TriggerHandlerMetadataProvider.cls
│ ├── TriggerHandlerMetadataProvider.cls-meta.xml
│ ├── TriggerHandlerTest.cls
│ └── TriggerHandlerTest.cls-meta.xml
│ └── objects
│ ├── Trigger_Handler_Method__mdt
│ ├── Trigger_Handler_Method__mdt.object-meta.xml
│ ├── fields
│ │ ├── Description__c.field-meta.xml
│ │ ├── Is_Enabled__c.field-meta.xml
│ │ ├── Order_of_execution__c.field-meta.xml
│ │ ├── Trigger_Event_Type__c.field-meta.xml
│ │ └── Trigger_Handler_Name__c.field-meta.xml
│ └── validationRules
│ │ └── Prevent_TestTriggerHandler_name.validationRule-meta.xml
│ └── Trigger_Handler_Settings__mdt
│ ├── Trigger_Handler_Settings__mdt.object-meta.xml
│ ├── fields
│ ├── Description__c.field-meta.xml
│ ├── Is_After_Delete_Enabled__c.field-meta.xml
│ ├── Is_After_Insert_Enabled__c.field-meta.xml
│ ├── Is_After_Undelete_Enabled__c.field-meta.xml
│ ├── Is_After_Update_Enabled__c.field-meta.xml
│ ├── Is_Before_Delete_Enabled__c.field-meta.xml
│ ├── Is_Before_Insert_Enabled__c.field-meta.xml
│ ├── Is_Before_Update_Enabled__c.field-meta.xml
│ └── Is_Globally_Enabled__c.field-meta.xml
│ └── validationRules
│ └── Prevent_TestTriggerHandler_record_name.validationRule-meta.xml
├── manifest
└── package.xml
└── sfdx-project.json
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | static-analysis:
4 | docker:
5 | - image: circleci/openjdk:latest
6 | environment:
7 | - PMD_URL: https://github.com/pmd/pmd/releases/download/pmd_releases/6.23.0/pmd-bin-6.23.0.zip
8 | steps:
9 | - checkout
10 | - restore_cache:
11 | keys:
12 | - pmd-v6.23.0
13 | - run:
14 | name: Install PMD
15 | command: |
16 | if [ ! -d pmd-bin-6.23.0 ]; then
17 | curl -L $PMD_URL -o pmd-bin-6.23.0.zip
18 | unzip pmd-bin-6.23.0.zip
19 | rm pmd-bin-6.23.0.zip
20 | fi
21 | - save_cache:
22 | key: pmd-v6.23.0
23 | paths:
24 | - pmd-bin-6.23.0
25 | - run:
26 | name: Run Static Analysis
27 | command: |
28 | pmd-bin-6.23.0/bin/run.sh pmd -d . -R assets/pmd-ruleset.xml -f text -l apex -r static-analysis.txt -cache nonexistingfile.cache
29 | - store_artifacts:
30 | path: static-analysis.txt
31 | unit-tests:
32 | docker:
33 | - image: circleci/openjdk:latest
34 | working_directory: ~/ci_app
35 | environment:
36 | - DX_CLI_URL: https://developer.salesforce.com/media/salesforce-cli/sfdx-linux-amd64.tar.xz
37 | steps:
38 | - checkout
39 | - restore_cache:
40 | keys:
41 | - sfdx-cli
42 | - run:
43 | name: Download CLI
44 | command: |
45 | if [ ! -d sfdx ]; then
46 | mkdir sfdx
47 | wget -qO- $DX_CLI_URL | tar xJ -C sfdx --strip-components 1
48 | fi
49 | - save_cache:
50 | key: sfdx-cli
51 | paths:
52 | - sfdx
53 | - run:
54 | name: Install CLI
55 | command: |
56 | ./sfdx/install
57 | sfdx
58 | - run:
59 | name: Create hub key
60 | command: |
61 | echo 'make hub key'
62 | openssl enc -nosalt -aes-256-cbc -d -in assets/server.key.enc -out assets/server.key -base64 -K $DECRYPTION_KEY -iv $DECRYPTION_IV
63 | - run:
64 | name: Create Scratch Org
65 | command: |
66 | sfdx force:auth:jwt:grant --clientid $HUB_CONSUMER_KEY --jwtkeyfile assets/server.key --username $HUB_SFDC_USER --setdefaultdevhubusername -a DevHub
67 | sfdx force:org:create -s -f config/project-scratch-def.json -a circle_build_$CIRCLE_BUILD_NUM --wait 5
68 | - run:
69 | name: Remove Server Key
70 | when: always
71 | command: |
72 | rm assets/server.key
73 | - run:
74 | name: Push Source
75 | command: |
76 | sfdx force:source:push -u circle_build_$CIRCLE_BUILD_NUM
77 | - run:
78 | name: Run Apex Tests
79 | command: |
80 | mkdir ~/tests
81 | mkdir ~/tests/apex
82 | sfdx force:apex:test:run -u circle_build_$CIRCLE_BUILD_NUM -c -r human -d ~/tests/apex -w 9999
83 | - run:
84 | name: Push to Codecov.io
85 | command: |
86 | cp ~/tests/apex/test-result-codecoverage.json .
87 | bash <(curl -s https://codecov.io/bash)
88 | - run:
89 | name: Clean Up
90 | when: always
91 | command: |
92 | sfdx force:org:delete -u circle_build_$CIRCLE_BUILD_NUM -p
93 | rm ~/tests/apex/*.txt ~/tests/apex/test-result-7*.json
94 | - store_artifacts:
95 | path: ~/tests
96 | - store_test_results:
97 | path: ~/tests
98 | workflows:
99 | version: 2
100 | continuous-integration:
101 | jobs:
102 | - static-analysis:
103 | filters:
104 | branches:
105 | ignore: example-of-usage
106 | - unit-tests:
107 | requires:
108 | - static-analysis
109 | filters:
110 | branches:
111 | ignore: example-of-usage
112 |
--------------------------------------------------------------------------------
/.forceignore:
--------------------------------------------------------------------------------
1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
3 | #
4 |
5 | # LWC configuration files
6 | **/jsconfig.json
7 | **/.eslintrc.json
8 |
9 | # LWC Jest
10 | **/__tests__/**
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.1.0
4 | - Migrated repo from MDAPI format to SFDX
5 |
6 | ## 1.0.0
7 | - First release, updated readme and examples of usage
8 |
9 | ## 0.9.0
10 | - Changed custom interfaces to Callable
11 |
12 | ## 0.7.0
13 | - Added TriggerHandler settings support
14 |
15 | ## 0.5.1
16 | - Updated README and example
17 |
18 | ## 0.5.0
19 | - Main functionality
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 AndreyFilonenko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Salesforce Apex trigger framework
2 |
3 | [](https://circleci.com/gh/AndreyFilonenko/sfdc-declarative-trigger-framework) [](https://codecov.io/gh/AndreyFilonenko/sfdc-declarative-trigger-framework)
4 |
5 |
6 |
8 |
9 |
10 | ## Overview
11 | A simple and minimal framework for Salesforce Apex triggers with declarative trigger handlers management - in accordance with Salesforce development best practices, defines a single entry point for sObject trigger with dispatching handler functions by a specific trigger event. Also gives an ability to manage trigger handlers with the no-code approach, just managing custom metadata definitions via point and clicks.
12 |
13 |
14 | There are three main blocks of functionality:
15 | * Trigger handler backbone with event handlers and dispatcher logic.
16 | * Trigger enablement logic, which allows you to disable the trigger hanler globally or by for a specific event (optionally).
17 | * Trigger handler logic management for determining the handlers and order of their execution for specific event (optionally).
18 |
19 |
20 | ## *TriggerHandler* public API
21 | #### Properties:
22 | * `List newList` - readonly, returns the Trigger.new records.
23 | * `Map newMap` - readonly, returns the Trigger.newMap records.
24 | * `List oldList` - readonly, returns the Trigger.old records.
25 | * `Map oldMap` - readonly, returns the Trigger.oldMap records.
26 | * `Integer size` - readonly, returns the quantity of trigger records.
27 |
28 | #### Exceptions
29 | * `TriggerHandlerException` - will be thrown in the next cases:
30 | 1. Call of the `newList` property on `Before Delete` or `After Delete` trigger events.
31 | 2. Call of the `newMap` property on `Before Insert`, `Before Delete` or `After Delete` trigger events.
32 | 3. Call of the `oldList` property on `Before Insert`, `After Insert` or `After Undelete` trigger events.
33 | 4. Call of the `oldMap` property on `Before Insert`, `After Insert` or `After Undelete` trigger events.
34 |
35 | #### Overridable methods
36 | Also, you can directly define all your logic in your sObject trigger handler just overriding the next methods:
37 |
38 | * `beforeInsert()`
39 | * `beforeUpdate()`
40 | * `beforeDelete()`
41 | * `afterInsert()`
42 | * `afterUpdate()`
43 | * `afterDelete()`
44 | * `afterUndelete()`
45 |
46 | But keep in mind - to prevent blocking of other features your method overrides must include base class method call, also your overriden functionality will be executed after all declarative handler methods ([see here](#Override-example)).
47 |
48 | ## Usage
49 | ### Main functionality
50 | First, create an Apex trigger for sObject:
51 | ```java
52 | trigger AccountTrigger on Account (before insert,
53 | before update,
54 | before delete,
55 | after insert,
56 | after update,
57 | after delete,
58 | after undelete) {
59 | new AccountTriggerHandler(
60 | Trigger.operationType,
61 | Trigger.new,
62 | Trigger.newMap,
63 | Trigger.old,
64 | Trigger.oldMap,
65 | Trigger.size
66 | ).dispatch();
67 | }
68 | ```
69 |
70 | Then, extend the base **TriggerHandler** class and create a trigger handler for your sObject:
71 | ```java
72 | public class AccountTriggerHandler extends TriggerHandler {
73 | public AccountTriggerHandler(System.TriggerOperation triggerOperation,
74 | List newList,
75 | Map newMap,
76 | List oldList,
77 | Map oldMap,
78 | Integer size) {
79 | super(
80 | triggerOperation,
81 | newList,
82 | newMap,
83 | oldList,
84 | oldMap,
85 | size
86 | );
87 | }
88 | }
89 | ```
90 |
91 | Finally, override the base methods with logic you need:
92 | ###### Override example
93 | ```java
94 | public class AccountTriggerHandler extends TriggerHandler {
95 | ...
96 |
97 | public override void beforeInsert() {
98 | // base method call
99 | super.beforeInsert();
100 |
101 | // put your overriden logic here...
102 | }
103 | }
104 | ```
105 | ### Trigger enablement management (optionally)
106 | Create a **Trigger_Handler_Settings__mdt** record to describe the sObject trigger behavior globally or on the specific event:
107 | 
108 | By default the trigger handler enabled globally with all events.
109 |
110 | ### Trigger handlers management (optionally)
111 | Define the handler with the next pattern (implement the [Callable interface](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_Callable.htm)):
112 | ```java
113 |
114 | public class AccountTriggerHandlerMethodBeforeInsert implements Callable {
115 | /**
116 | * See possible params values below:
117 | * @param action - will contain the string name of TriggerOperation enum value for the current context
118 | * @param args - will contain a map of Trigger props with the prop names as keys
119 | * For example, you can retrieve newList by the key 'newList' for BEFORE_INSERT event handler
120 | */
121 | Object call(String action, Map args) {
122 | // put your logic here...
123 | }
124 | }
125 | ```
126 |
127 | ... and register it in the Custom metadata by creating the correspondent **Trigger_Handler_Method__mdt** record:
128 | 
129 |
130 | #### Please see the detailed example of the usage [here!](https://github.com/AndreyFilonenko/sfdc-declarative-trigger-framework/tree/example-of-usage)
131 |
132 | ## Change log
133 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
134 |
135 | ## References
136 | The idea of this framework was based on the main concepts of [TDTM](https://powerofus.force.com/s/article/EDA-TDTM-Overview "TDTM Overview") framework and Kevin O'Hara`s [sfdc-trigger-framework](https://github.com/kevinohara80/sfdc-trigger-framework "sfdc-trigger-framework").
137 |
138 | ## License
139 | The MIT License (MIT). Please see [License File](LICENSE) for more information.
140 |
--------------------------------------------------------------------------------
/assets/pmd-ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | Quickstart configuration of PMD for Salesforce.com Apex. Includes the rules that are most likely to apply everywhere.
7 |
8 |
9 |
10 | 3
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 3
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 3
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 3
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 3
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 3
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 3
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | 3
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | 3
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | 3
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | 3
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | 3
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | 3
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | 3
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | 3
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | 3
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | 3
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | 3
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | 3
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | 3
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | 3
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | 3
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 | 3
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | 3
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 | 3
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 | 3
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
282 |
283 | 3
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 | 3
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 | 3
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 | 3
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 | 3
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 | 3
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 | 3
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
353 |
354 | 3
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 | 3
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 | 3
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 | 3
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 | 3
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 | 3
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 | 3
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 | 3
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 | 3
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 | 3
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 | 3
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 | 3
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
478 |
479 |
480 |
481 | 3
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
--------------------------------------------------------------------------------
/assets/server.key.enc:
--------------------------------------------------------------------------------
1 | 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
36 |
--------------------------------------------------------------------------------
/config/project-scratch-def.json:
--------------------------------------------------------------------------------
1 | {
2 | "orgName": "Demo company",
3 | "edition": "Developer",
4 | "features": []
5 | }
6 |
--------------------------------------------------------------------------------
/force-app/default/main/classes/TriggerHandler.cls:
--------------------------------------------------------------------------------
1 | public virtual class TriggerHandler {
2 | @TestVisible
3 | private System.TriggerOperation triggerOperation;
4 | @TestVisible
5 | private TriggerHandlerMetadataProvider metadataProvider = new TriggerHandlerMetadataProvider();
6 | private String triggerHandlerName;
7 | private Trigger_Handler_Settings__mdt triggerHandlerSettings;
8 |
9 | protected TriggerHandler(System.TriggerOperation triggerOperation,
10 | List newList,
11 | Map newMap,
12 | List oldList,
13 | Map oldMap,
14 | Integer size) {
15 | this.triggerOperation = triggerOperation;
16 | this.newList = newList;
17 | this.newMap = newMap;
18 | this.oldList = oldList;
19 | this.oldMap = oldMap;
20 | this.size = size;
21 | this.triggerHandlerName = String.valueOf(this).substring(0, String.valueOf(this).indexOf(':'));
22 | }
23 |
24 | @testVisible
25 | protected List newList {
26 | get {
27 | if (triggerOperation == System.TriggerOperation.BEFORE_DELETE ||
28 | triggerOperation == System.TriggerOperation.AFTER_DELETE) {
29 | throw new TriggerHandlerException('Trigger.new is not available for current operation: ' + triggerOperation.name());
30 | }
31 | return this.newList;
32 | }
33 | private set;
34 | }
35 |
36 | @testVisible
37 | protected Map newMap {
38 | get {
39 | if (triggerOperation == System.TriggerOperation.BEFORE_INSERT ||
40 | triggerOperation == System.TriggerOperation.BEFORE_DELETE ||
41 | triggerOperation == System.TriggerOperation.AFTER_DELETE) {
42 | throw new TriggerHandlerException('Trigger.newMap is not available for current operation: ' + triggerOperation.name());
43 | }
44 | return this.newMap;
45 | }
46 | private set;
47 | }
48 |
49 | @testVisible
50 | protected List oldList {
51 | get {
52 | if (triggerOperation == System.TriggerOperation.BEFORE_INSERT ||
53 | triggerOperation == System.TriggerOperation.AFTER_INSERT ||
54 | triggerOperation == System.TriggerOperation.AFTER_UNDELETE) {
55 | throw new TriggerHandlerException('Trigger.old is not available for current operation: ' + triggerOperation.name());
56 | }
57 | return this.oldList;
58 | }
59 | private set;
60 | }
61 |
62 | @testVisible
63 | protected Map oldMap {
64 | get {
65 | if (triggerOperation == System.TriggerOperation.BEFORE_INSERT ||
66 | triggerOperation == System.TriggerOperation.AFTER_INSERT ||
67 | triggerOperation == System.TriggerOperation.AFTER_UNDELETE) {
68 | throw new TriggerHandlerException('Trigger.oldMap is not available for current operation: ' + triggerOperation.name());
69 | }
70 | return this.oldMap;
71 | }
72 | private set;
73 | }
74 |
75 | @testVisible
76 | protected Integer size { protected get; private set; }
77 |
78 | public void dispatch() {
79 | if (isTriggerEnabled()) {
80 | switch on this.triggerOperation {
81 | when BEFORE_INSERT {
82 | this.beforeInsert();
83 | }
84 | when AFTER_INSERT {
85 | this.afterInsert();
86 | }
87 | when BEFORE_UPDATE {
88 | this.beforeUpdate();
89 | }
90 | when AFTER_UPDATE {
91 | this.afterUpdate();
92 | }
93 | when BEFORE_DELETE {
94 | this.beforeDelete();
95 | }
96 | when AFTER_DELETE {
97 | this.afterDelete();
98 | }
99 | when AFTER_UNDELETE {
100 | this.afterUndelete();
101 | }
102 | }
103 | }
104 | }
105 |
106 | @TestVisible
107 | protected virtual void beforeInsert() {
108 | executeHandlerMethods(this.triggerOperation.name(), this.newList, null);
109 | }
110 |
111 | @TestVisible
112 | protected virtual void afterInsert() {
113 | executeHandlerMethods(this.triggerOperation.name(), this.newList, null);
114 | }
115 |
116 | @TestVisible
117 | protected virtual void beforeUpdate() {
118 | executeHandlerMethods(this.triggerOperation.name(), this.newList, this.oldList);
119 | }
120 |
121 | @TestVisible
122 | protected virtual void afterUpdate() {
123 | executeHandlerMethods(this.triggerOperation.name(), this.newList, this.oldList);
124 | }
125 |
126 | @TestVisible
127 | protected virtual void beforeDelete() {
128 | executeHandlerMethods(this.triggerOperation.name(), null, this.oldList);
129 | }
130 |
131 | @TestVisible
132 | protected virtual void afterDelete() {
133 | executeHandlerMethods(this.triggerOperation.name(), null, this.oldList);
134 | }
135 |
136 | @TestVisible
137 | protected virtual void afterUndelete() {
138 | executeHandlerMethods(this.triggerOperation.name(), this.newList, null);
139 | }
140 |
141 | private Boolean isTriggerEnabled() {
142 | Boolean isTriggerEnabled = true;
143 | List handlerSettings = this.metadataProvider.getHandlerSettings(this.triggerHandlerName);
144 | if (!handlerSettings.isEmpty()) {
145 | isTriggerEnabled = false;
146 | Trigger_Handler_Settings__mdt currentHandlerSettings = handlerSettings.get(0);
147 | if (currentHandlerSettings.Is_Globally_Enabled__c) {
148 | isTriggerEnabled = true;
149 | switch on this.triggerOperation {
150 | when BEFORE_INSERT {
151 | isTriggerEnabled = currentHandlerSettings.Is_Before_Insert_Enabled__c;
152 | }
153 | when AFTER_INSERT {
154 | isTriggerEnabled = currentHandlerSettings.Is_After_Insert_Enabled__c;
155 | }
156 | when BEFORE_UPDATE {
157 | isTriggerEnabled = currentHandlerSettings.Is_Before_Update_Enabled__c;
158 | }
159 | when AFTER_UPDATE {
160 | isTriggerEnabled = currentHandlerSettings.Is_After_Update_Enabled__c;
161 | }
162 | when BEFORE_DELETE {
163 | isTriggerEnabled = currentHandlerSettings.Is_Before_Delete_Enabled__c;
164 | }
165 | when AFTER_DELETE {
166 | isTriggerEnabled = currentHandlerSettings.Is_After_Delete_Enabled__c;
167 | }
168 | when AFTER_UNDELETE {
169 | isTriggerEnabled = currentHandlerSettings.Is_After_Undelete_Enabled__c;
170 | }
171 | }
172 | }
173 | }
174 |
175 | return isTriggerEnabled;
176 | }
177 |
178 | private List getHandlerMethodNames() {
179 | List handlerMethods = this.metadataProvider.getHandlerMethods(this.triggerHandlerName, this.triggerOperation);
180 | List handlerMethodWrappers = new List();
181 | for (Trigger_Handler_Method__mdt handlerMethod : handlerMethods) {
182 | handlerMethodWrappers.add(new TriggerHandlerMethodWrapper(
183 | handlerMethod.DeveloperName,
184 | handlerMethod.Order_of_execution__c
185 | ));
186 | }
187 |
188 | handlerMethodWrappers.sort();
189 |
190 | List handlerMethodNames = new List();
191 | for (Integer i = 0; i < handlerMethodWrappers.size(); i++) {
192 | handlerMethodNames.add(handlerMethodWrappers[i].handlerMethodName);
193 | }
194 |
195 | return handlerMethodNames;
196 | }
197 |
198 | private void executeHandlerMethods(String context, List newList, List oldList) {
199 | List handlerMethodNames = getHandlerMethodNames();
200 |
201 | if (!handlerMethodNames.isEmpty()) {
202 | for (Integer i = 0; i < handlerMethodNames.size(); i++) {
203 | if (Type.forName(handlerMethodNames[i]) != null) {
204 | Map argsMap = new Map();
205 | if (newList != null) {
206 | argsMap.put('newList', newList);
207 | }
208 | if (oldList != null) {
209 | argsMap.put('oldList', oldList);
210 | }
211 |
212 | Type handlerMethodType = Type.forName(handlerMethodNames[i]);
213 | Callable currentHandlerMethod = (Callable)handlerMethodType.newInstance();
214 | currentHandlerMethod.call(context, argsMap);
215 | }
216 | }
217 | }
218 | }
219 |
220 | public class TriggerHandlerException extends Exception {}
221 |
222 | private class TriggerHandlerMethodWrapper implements Comparable {
223 | TriggerHandlerMethodWrapper(String handlerMethodName, Decimal executionOrder) {
224 | this.handlerMethodName = handlerMethodName;
225 | this.executionOrder = Integer.valueOf(executionOrder);
226 | }
227 |
228 | public String handlerMethodName { get; private set; }
229 | public Integer executionOrder { get; private set; }
230 |
231 | public Integer compareTo(Object compareTo) {
232 | TriggerHandlerMethodWrapper compareToMethodWrapper = (TriggerHandlerMethodWrapper)compareTo;
233 | Integer returnValue = 0;
234 | if (executionOrder > compareToMethodWrapper.executionOrder) {
235 | returnValue = 1;
236 | } else if (executionOrder < compareToMethodWrapper.executionOrder) {
237 | returnValue = -1;
238 | }
239 |
240 | return returnValue;
241 | }
242 | }
243 | }
--------------------------------------------------------------------------------
/force-app/default/main/classes/TriggerHandler.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 50.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/default/main/classes/TriggerHandlerMetadataProvider.cls:
--------------------------------------------------------------------------------
1 | public class TriggerHandlerMetadataProvider {
2 | public List getHandlerMethods(String triggerHandlerName,
3 | System.TriggerOperation triggerOperation) {
4 | return [
5 | SELECT Id,
6 | DeveloperName,
7 | Order_of_execution__c
8 | FROM Trigger_Handler_Method__mdt
9 | WHERE Trigger_Handler_Name__c = :triggerHandlerName
10 | AND Trigger_Event_Type__c = :triggerOperation.name()
11 | AND Is_Enabled__c = true
12 | ];
13 | }
14 |
15 | public List getHandlerSettings(String triggerHandlerName) {
16 | return [
17 | SELECT Id,
18 | Is_Globally_Enabled__c,
19 | Is_Before_Insert_Enabled__c,
20 | Is_After_Insert_Enabled__c,
21 | Is_Before_Update_Enabled__c,
22 | Is_After_Update_Enabled__c,
23 | Is_Before_Delete_Enabled__c,
24 | Is_After_Delete_Enabled__c,
25 | Is_After_Undelete_Enabled__c
26 | FROM Trigger_Handler_Settings__mdt
27 | WHERE DeveloperName = :triggerHandlerName
28 | ];
29 | }
30 | }
--------------------------------------------------------------------------------
/force-app/default/main/classes/TriggerHandlerMetadataProvider.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 50.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/default/main/classes/TriggerHandlerTest.cls:
--------------------------------------------------------------------------------
1 | @isTest
2 | public class TriggerHandlerTest {
3 | private static final String TRIGGER_HANDLER_NAME = 'TestTriggerHandler';
4 | private static String lastMethodCalled;
5 | private static TriggerHandlerTest.TestTriggerHandler handler = new TriggerHandlerTest.TestTriggerHandler();
6 |
7 | @isTest
8 | private static void testPropertyNewList() {
9 | List newList;
10 | handler.triggerOperation = System.TriggerOperation.BEFORE_DELETE;
11 | try {
12 | newList = handler.newList;
13 | } catch (TriggerHandler.TriggerHandlerException ex) {
14 | System.assertNotEquals(null, ex.getMessage());
15 | }
16 |
17 | handler.triggerOperation = System.TriggerOperation.AFTER_DELETE;
18 | try {
19 | newList = handler.newList;
20 | } catch (TriggerHandler.TriggerHandlerException ex) {
21 | System.assertNotEquals(null, ex.getMessage());
22 | }
23 |
24 | handler.triggerOperation = System.TriggerOperation.BEFORE_INSERT;
25 | newList = handler.newList;
26 | System.assertNotEquals(null, newList);
27 | }
28 |
29 | @isTest
30 | private static void testPropertyNewMap() {
31 | Map newMap;
32 | handler.triggerOperation = System.TriggerOperation.BEFORE_INSERT;
33 | try {
34 | newMap = handler.newMap;
35 | } catch (TriggerHandler.TriggerHandlerException ex) {
36 | System.assertNotEquals(null, ex.getMessage());
37 | }
38 |
39 | handler.triggerOperation = System.TriggerOperation.BEFORE_DELETE;
40 | try {
41 | newMap = handler.newMap;
42 | } catch (TriggerHandler.TriggerHandlerException ex) {
43 | System.assertNotEquals(null, ex.getMessage());
44 | }
45 |
46 | handler.triggerOperation = System.TriggerOperation.AFTER_DELETE;
47 | try {
48 | newMap = handler.newMap;
49 | } catch (TriggerHandler.TriggerHandlerException ex) {
50 | System.assertNotEquals(null, ex.getMessage());
51 | }
52 |
53 | handler.triggerOperation = System.TriggerOperation.BEFORE_UPDATE;
54 | newMap = handler.newMap;
55 | System.assertNotEquals(null, newMap);
56 | }
57 |
58 | @isTest
59 | private static void testPropertyOldList() {
60 | List oldList;
61 | handler.triggerOperation = System.TriggerOperation.BEFORE_INSERT;
62 | try {
63 | oldList = handler.oldList;
64 | } catch (TriggerHandler.TriggerHandlerException ex) {
65 | System.assertNotEquals(null, ex.getMessage());
66 | }
67 |
68 | handler.triggerOperation = System.TriggerOperation.AFTER_INSERT;
69 | try {
70 | oldList = handler.oldList;
71 | } catch (TriggerHandler.TriggerHandlerException ex) {
72 | System.assertNotEquals(null, ex.getMessage());
73 | }
74 |
75 | handler.triggerOperation = System.TriggerOperation.AFTER_UNDELETE;
76 | try {
77 | oldList = handler.oldList;
78 | } catch (TriggerHandler.TriggerHandlerException ex) {
79 | System.assertNotEquals(null, ex.getMessage());
80 | }
81 |
82 | handler.triggerOperation = System.TriggerOperation.BEFORE_UPDATE;
83 | oldList = handler.oldList;
84 | System.assertNotEquals(null, oldList);
85 | }
86 |
87 | @isTest
88 | private static void testPropertyOldMap() {
89 | Map oldMap;
90 | handler.triggerOperation = System.TriggerOperation.BEFORE_INSERT;
91 | try {
92 | oldMap = handler.oldMap;
93 | } catch (TriggerHandler.TriggerHandlerException ex) {
94 | System.assertNotEquals(null, ex.getMessage());
95 | }
96 |
97 | handler.triggerOperation = System.TriggerOperation.AFTER_INSERT;
98 | try {
99 | oldMap = handler.oldMap;
100 | } catch (TriggerHandler.TriggerHandlerException ex) {
101 | System.assertNotEquals(null, ex.getMessage());
102 | }
103 |
104 | handler.triggerOperation = System.TriggerOperation.AFTER_UNDELETE;
105 | try {
106 | oldMap = handler.oldMap;
107 | } catch (TriggerHandler.TriggerHandlerException ex) {
108 | System.assertNotEquals(null, ex.getMessage());
109 | }
110 |
111 | handler.triggerOperation = System.TriggerOperation.BEFORE_UPDATE;
112 | oldMap = handler.oldMap;
113 | System.assertNotEquals(null, oldMap);
114 | }
115 |
116 | @isTest
117 | private static void testPropertySize() {
118 | Integer size = handler.size;
119 | System.assertEquals(10, size);
120 | }
121 |
122 | @isTest
123 | private static void testDispatch() {
124 | handler.triggerOperation = System.TriggerOperation.AFTER_UNDELETE;
125 |
126 | Test.startTest();
127 | handler.dispatch();
128 | Test.stopTest();
129 |
130 | System.assertEquals(null, lastMethodCalled);
131 | }
132 |
133 | @isTest
134 | private static void testBeforeInsert() {
135 | handler.triggerOperation = System.TriggerOperation.BEFORE_INSERT;
136 | handler.metadataProvider = (TriggerHandlerMetadataProvider)Test.createStub(
137 | TriggerHandlerMetadataProvider.class,
138 | new TriggerHandlerMetadataProviderStub()
139 | );
140 |
141 | Test.startTest();
142 | handler.dispatch();
143 | Test.stopTest();
144 |
145 | System.assertEquals('beforeInsert3', lastMethodCalled);
146 | }
147 |
148 | @isTest
149 | private static void testAfterInsert() {
150 | handler.triggerOperation = System.TriggerOperation.AFTER_INSERT;
151 | handler.metadataProvider = (TriggerHandlerMetadataProvider)Test.createStub(
152 | TriggerHandlerMetadataProvider.class,
153 | new TriggerHandlerMetadataProviderStub()
154 | );
155 |
156 | Test.startTest();
157 | handler.dispatch();
158 | Test.stopTest();
159 |
160 | System.assertEquals('afterInsert', lastMethodCalled);
161 | }
162 |
163 | @isTest
164 | private static void testBeforeUpdate() {
165 | handler.triggerOperation = System.TriggerOperation.BEFORE_UPDATE;
166 | handler.metadataProvider = (TriggerHandlerMetadataProvider)Test.createStub(
167 | TriggerHandlerMetadataProvider.class,
168 | new TriggerHandlerMetadataProviderStub()
169 | );
170 |
171 | Test.startTest();
172 | handler.dispatch();
173 | Test.stopTest();
174 |
175 | System.assertEquals('beforeUpdate', lastMethodCalled);
176 | }
177 |
178 | @isTest
179 | private static void testAfterUpdate() {
180 | handler.triggerOperation = System.TriggerOperation.AFTER_UPDATE;
181 | handler.metadataProvider = (TriggerHandlerMetadataProvider)Test.createStub(
182 | TriggerHandlerMetadataProvider.class,
183 | new TriggerHandlerMetadataProviderStub()
184 | );
185 |
186 | Test.startTest();
187 | handler.dispatch();
188 | Test.stopTest();
189 |
190 | System.assertEquals('afterUpdate', lastMethodCalled);
191 | }
192 |
193 | @isTest
194 | private static void testBeforeDelete() {
195 | handler.triggerOperation = System.TriggerOperation.BEFORE_DELETE;
196 | handler.metadataProvider = (TriggerHandlerMetadataProvider)Test.createStub(
197 | TriggerHandlerMetadataProvider.class,
198 | new TriggerHandlerMetadataProviderStub()
199 | );
200 |
201 | Test.startTest();
202 | handler.dispatch();
203 | Test.stopTest();
204 |
205 | System.assertEquals('beforeDelete', lastMethodCalled);
206 | }
207 |
208 | @isTest
209 | private static void testAfterDelete() {
210 | handler.triggerOperation = System.TriggerOperation.AFTER_DELETE;
211 | handler.metadataProvider = (TriggerHandlerMetadataProvider)Test.createStub(
212 | TriggerHandlerMetadataProvider.class,
213 | new TriggerHandlerMetadataProviderStub()
214 | );
215 |
216 | Test.startTest();
217 | handler.dispatch();
218 | Test.stopTest();
219 |
220 | System.assertEquals('afterDelete', lastMethodCalled);
221 | }
222 |
223 | @isTest
224 | private static void testAfterUndelete() {
225 | handler.triggerOperation = System.TriggerOperation.AFTER_UNDELETE;
226 | handler.metadataProvider = (TriggerHandlerMetadataProvider)Test.createStub(
227 | TriggerHandlerMetadataProvider.class,
228 | new TriggerHandlerMetadataProviderStub()
229 | );
230 |
231 | Test.startTest();
232 | handler.dispatch();
233 | Test.stopTest();
234 |
235 | System.assertEquals('afterUndelete', lastMethodCalled);
236 | }
237 |
238 | private class TestTriggerHandler extends TriggerHandler {
239 | public TestTriggerHandler() {
240 | super(
241 | null,
242 | new List(),
243 | new Map(),
244 | new List(),
245 | new Map(),
246 | 10
247 | );
248 | }
249 | }
250 |
251 | private class TriggerHandlerMetadataProviderStub implements System.StubProvider {
252 | public Object handleMethodCall(Object stubbedObject,
253 | String stubbedMethodName,
254 | Type returnType,
255 | List listOfParamTypes,
256 | List listOfParamNames,
257 | List