├── .forceignore
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── apex-trigger-handler
└── main
│ └── default
│ └── classes
│ ├── Triggers.cls
│ ├── Triggers.cls-meta.xml
│ ├── TriggersTest.cls
│ └── TriggersTest.cls-meta.xml
├── apex-trigger-loader
└── main
│ └── default
│ ├── classes
│ ├── TriggersLoader.cls
│ ├── TriggersLoader.cls-meta.xml
│ ├── TriggersLoaderTest.cls
│ └── TriggersLoaderTest.cls-meta.xml
│ ├── layouts
│ └── Apex_Trigger_Handler_Setting__mdt-Apex Trigger Handler Setting Layout.layout-meta.xml
│ └── objects
│ └── Apex_Trigger_Handler_Setting__mdt
│ ├── Apex_Trigger_Handler_Setting__mdt.object-meta.xml
│ └── fields
│ ├── Active__c.field-meta.xml
│ ├── Execution_Order__c.field-meta.xml
│ ├── Handler_Class__c.field-meta.xml
│ ├── SObject__c.field-meta.xml
│ ├── Tag__c.field-meta.xml
│ └── Trigger_Event__c.field-meta.xml
├── config
└── project-scratch-def.json
├── docs
└── images
│ └── deploy-button.png
├── package.json
├── script.sh
└── sfdx-project.json
/.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 | profiles
6 | customMetadata
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore.
2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore
3 | # For useful gitignore templates see: https://github.com/github/gitignore
4 |
5 | # Salesforce cache
6 | .sf/
7 | .sfdx/
8 | .localdevserver/
9 | deploy-options.json
10 |
11 | # LWC VSCode autocomplete
12 | **/lwc/jsconfig.json
13 |
14 | # LWC Jest coverage reports
15 | coverage/
16 |
17 | # Logs
18 | logs
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | # Dependency directories
25 | node_modules/
26 |
27 | # Eslint cache
28 | .eslintcache
29 |
30 | # MacOS system files
31 | .DS_Store
32 |
33 | # Windows system files
34 | Thumbs.db
35 | ehthumbs.db
36 | [Dd]esktop.ini
37 | $RECYCLE.BIN/
38 |
39 | # Local environment variables
40 | .env
41 | .vscode
42 | .husky
43 | package-lock.json
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # List files or directories below to ignore them when running prettier
2 | # More information: https://prettier.io/docs/en/ignore.html
3 | #
4 |
5 | **/staticresources/**
6 | .localdevserver
7 | .sfdx
8 | .vscode
9 |
10 | coverage/
11 |
12 | TriggersTest.cls
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "overrides": [
4 | {
5 | "files": "*.cls",
6 | "options": { "tabWidth": 4, "printWidth": 120 }
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2020, Jianfeng Jin
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Apex Trigger Handler
2 |
3 |   
4 |
5 | There are already many trigger handler libraries out there, but this one has some different approaches or advantages such as state sharing, built in helper methods etc.
6 |
7 | ### Features
8 |
9 | 1. Built-in helpers to perform common operations on trigger properties, such as detect field changes.
10 | 2. Control flow of handler execution with `context.next()`, `context.stop()`, and `context.skips`.
11 | 3. Optionally register and control handlers with custom metadata type settings.
12 |
13 | ### Package ApexTriggerHandler
14 |
15 | This package is the minimal installation which only includes two classes `Triggers.cls` and `TriggersTest.cls`.
16 |
17 | | Environment | Installation Link | Version |
18 | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
19 | | Production, Developer | | ver 1.2.1 |
20 | | Sandbox | | ver 1.2.1 |
21 |
22 | ### Package ApexTriggerHandlerExt
23 |
24 | This package can be optionally installed to extend a new feature ([custom metadata type settings](#12-bind-with-handler-settings)) for the above one. It introduces additional but only one SOQL query to a custom metadata type. If your system already reaches some governor limit around SOQL queries, can consider deploy this one later. **Note**: The above package is required to be installed before this one.
25 |
26 | | Environment | Installation Link | Version |
27 | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
28 | | Production, Developer | | ver 1.2.1 |
29 | | Sandbox | | ver 1.2.1 |
30 |
31 | ---
32 |
33 | ### v1.2 Release Notes
34 |
35 | - Support custom metadata type settings to register trigger handlers. ([jump to section](#12-bind-with-handler-settings))
36 | - **Improve Consistency** (v1.2.1): Ids returned by `props.filterChangedAny` and `props.filterChangedAll` are now in the same Id orders of `props.newList`.
37 |
38 | ---
39 |
40 | ## Table of Contents
41 |
42 | - [1. Trigger](#1-trigger)
43 | - [1.1 Bind with Handler Instances](#11-bind-with-handler-instances)
44 | - [1.2 Bind with Handler Settings](#12-bind-with-handler-settings)
45 | - [1.3 Bind with DI Framework](#13-bind-with-di-framework)
46 | - [2. Trigger Handler](#2-trigger-handler)
47 | - [2.1 Create Handlers](#21-create-handlers)
48 | - [2.2 Skip Handlers](#22-skip-handlers)
49 | - [3. Tests](#3-tests)
50 | - [3.1 Test with Mockup Data](#31-test-with-mockup-data)
51 | - [3.2 Test with Mockup Library](#32-test-with-mockup-library)
52 | - [4. APIs](#4-apis)
53 | - [4.1 Trigger Handler Interfaces](#41-trigger-handler-interfaces)
54 | - [4.2 Triggers.Context](#42-triggerscontext)
55 | - [4.3 Triggers.Props](#43-triggersprops)
56 | - [5. License](#5-license)
57 |
58 | ## 1. Trigger
59 |
60 | ### 1.1 Bind with Handler Instances
61 |
62 | This is an example about how handlers can be registered in triggers. As you have noticed, we are creating same handlers for different trigger events. This is because handlers may need to execute in different orders for different trigger events, we need to provide developers great controls over the order of executions.
63 |
64 | ```java
65 | trigger AccountTrigger on Account (before update, after update) {
66 | Triggers.prepare()
67 | .beforeUpdate()
68 | .bind(new MyAccountHandler())
69 | .bind(new AnotherAccountHandler())
70 | .afterUpdate()
71 | .bind(new AnotherAccountHandler())
72 | .bind(new MyAccountHandler())
73 | .execute();
74 | }
75 | ```
76 |
77 | ### 1.2 Bind with Handler Settings
78 |
79 | This feature is only available when `ApexTriggerHandlerExt` package is installed, or its metadata is manually deployed. Here are some sample records of `Apex_Trigger_Handler_Setting__mdt`, which can provide fine-grained control of trigger handler behaviors at runtime, such as:
80 |
81 | 1. To register trigger handlers for a particular sObject trigger event (`SObject__c`, `Trigger_Event__c`, `Handler_Class__c`).
82 | 2. To activate or deactivate trigger handlers (`Active__c`).
83 | 3. To reorder trigger handlers (`Execution_Order__c`).
84 | 4. To optionally group trigger handlers with tags (`Tag__c`).
85 |
86 | | SObject\_\_c | Trigger_Event\_\_c | Handler_Class\_\_c | Execution_Order\_\_c | Tag\_\_c | Active\_\_c |
87 | | ------------ | ------------------ | ---------------------- | -------------------- | -------- | ----------- |
88 | | Account | BEFORE_UPDATE | AccountTriggerHandler1 | 1 | tag1 | TRUE |
89 | | Account | BEFORE_UPDATE | AccountTriggerHandler2 | 2 | | TRUE |
90 | | Account | BEFORE_UPDATE | AccountTriggerHandler3 | 3 | | **FALSE** |
91 | | Account | AFTER_UPDATE | AccountTriggerHandler4 | 1 | tag1 | TRUE |
92 | | Account | AFTER_UPDATE | AccountTriggerHandler5 | 2 | tag2 | TRUE |
93 | | Account | AFTER_UPDATE | AccountTriggerHandler6 | 3 | tag2 | TRUE |
94 |
95 | Two additional APIs are provided to load the handlers from the above settings, `load()` and `load(tag)`. Their usages are explained in the following comments.
96 |
97 | ```java
98 | trigger AccountTrigger on Account (before update, after update) {
99 | Triggers.prepare()
100 | .beforeUpdate()
101 | .bind(new MyAccountHandler())
102 | .load() // load all active handlers under Account BEFORE_UPDATE
103 | // - AccountTriggerHandler1
104 | // - AccountTriggerHandler2
105 | .bind(new AnotherAccountHandler())
106 | .afterUpdate()
107 | .bind(new AnotherAccountHandler())
108 | .load('tag1') // load all active handlers with 'tag1' under Account AFTER_UPDATE
109 | // - AccountTriggerHandler4
110 | .bind(new MyAccountHandler())
111 | .load('tag2') // load all active handlers with 'tag2' under Account AFTER_UPDATE
112 | // - AccountTriggerHandler5
113 | // - AccountTriggerHandler6
114 | .execute();
115 | }
116 | ```
117 |
118 | ### 1.3 Bind with DI Framework
119 |
120 | The following demo is using [Apex DI](https://github.com/apexfarm/ApexDI) as a dependency injection (DI) framework.
121 |
122 | ```java
123 | trigger AccountTrigger on Account (before update, after update) {
124 | // reference interfaces and decouple trigger from implementations
125 | DI.Module salesModule = DI.getModule(SalesModule.class);
126 | Triggers.prepare()
127 | .beforeUpdate()
128 | .bind((Triggers.Handler) salesModule.getService(IMyAccountHandler.class))
129 | .bind((Triggers.Handler) salesModule.getService(IAnotherAccountHandler.class))
130 | .afterUpdate()
131 | .bind((Triggers.Handler) salesModule.getService(IAnotherAccountHandler.class))
132 | .bind((Triggers.Handler) salesModule.getService(IMyAccountHandler.class))
133 | .execute();
134 | }
135 |
136 | public class SalesModule extends DI.Module {
137 | // register handler implementation against interfaces
138 | protected overried void configure(DI.ServiceCollection services) {
139 | services.addTransient('IMyAccountHandler', 'MyAccountHandler');
140 | services.addTransient('IAnotherAccountHandler', 'AnotherAccountHandler');
141 | }
142 | }
143 |
144 | public class IMyAccountHandler extends Triggers.Handler {}
145 | public class MyAccountHandler implements
146 | IMyAccountHandler, Triggers.BeforeUpdate, Triggers.AfterUpdate {}
147 |
148 | public class IAnotherAccountHandler extends Triggers.Handler {}
149 | public class AnotherAccountHandler implements
150 | IAnotherAccountHandler, Triggers.BeforeUpdate, Triggers.AfterUpdate {}
151 | ```
152 |
153 | ## 2. Trigger Handler
154 |
155 | ### 2.1 Create Handlers
156 |
157 | To create a trigger handler, you will need to create a class that implements the `Triggers.Handler` interface and its `criteria` method. Please check the comments below for detailed explanations and tricks to customize a trigger handler.
158 |
159 | ```java
160 | // 1. Use interfaces instead of a base class to extend a custom handler. With interface
161 | // approach we can declare only the needed interfaces explicitly, which is much cleaner
162 | // and clearer.
163 | public class MyAccountHandler implements Triggers.Handler,
164 | Triggers.BeforeUpdate,
165 | Triggers.AfterUpdate {
166 |
167 | // 2. There is a "criteria" stage before any handler execution. This gives
168 | // developers opportunities to turn on and off the handlers according to
169 | // configurations at run time.
170 | public Boolean criteria(Triggers.Context context) {
171 | return Triggers.WHEN_ALWAYS;
172 |
173 | // 3. There are also helper methods to check if certain fields have changes
174 | // return context.props.isChangedAny(Account.Name, Account.Description);
175 | // return context.props.isChangedAll(Account.Name, Account.Description);
176 | }
177 |
178 | public void beforeUpdate(Triggers.Context context) {
179 | then(context);
180 | }
181 |
182 | public void afterUpdate(Triggers.Context context) {
183 | then(context);
184 | }
185 |
186 | private void then(Triggers.Context context) {
187 | // 4. All properties on Trigger have been exposed to context.props.
188 | // Direct reference of Trigger.old and Trigger.new should be avoided,
189 | // instead use context.props.oldList and context.props.newList.
190 | if (context.props.isUpdate) {
191 |
192 | // 5. Use context.state to pass query or computation results down to all
193 | // following handlers within the current trigger context, i.e. before update.
194 | // Before update and after update are considered as differenet contexts.
195 | Integer counter = (Integer) context.state.get('counter');
196 | if (counter == null) {
197 | context.state.put('counter', 0);
198 | } else {
199 | context.state.put('counter', counter + 1);
200 | }
201 |
202 | // 6. Use context.skips or Triggers.skips to prevent specific handlers from
203 | // execution. Please do remember restore the handler when appropriate.
204 | context.skips.add(ContactHandler.class);
205 | List contacts = ...;
206 | Database.insert(contacts);
207 | context.skips.remove(ContactHandler.class);
208 |
209 | // 7-1. Call context.next() to execute the next handler. It is optional to use,
210 | // unless some following up logics need to be performed after all following
211 | // handlers finished.
212 | context.next();
213 |
214 | // 7-2. If context.stop() is called instead of context.next(), any following
215 | // handlers won't be executed, just like the STOP in process builder.
216 | context.stop();
217 | }
218 | }
219 | }
220 | ```
221 |
222 | ### 2.2 Skip Handlers
223 |
224 | Global static variable `Triggers.skips` references the same `context.skips`, so you can use it to skip handlers outside of the handler contexts. For example, when you want to skip a trigger handler in a batch class:
225 |
226 | ```java
227 | global class AccountUpdateBatch implements Database.Batchable {
228 | ...
229 | global void execute(Database.BatchableContext BC, List scope){
230 | Triggers.skips.add(MyAccountHandler.class);
231 | // Update accounts...
232 | Triggers.skips.remove(MyAccountHandler.class);
233 | }
234 | ...
235 | }
236 | ```
237 |
238 | ## 3. Tests
239 |
240 | ### 3.1 Test with Mockup Data
241 |
242 | The following method is private but `@TestVisible`, it can be used in test methods to supply mockup records for old and new lists. So we don't need to perform DMLs to trigger the handlers.
243 |
244 | ```java
245 | @isTest
246 | static void test_AccountTriggerHandler_BeforeUpdate {
247 | List oldList = new List {
248 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'Old Name 1'),
249 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'Old Name 2'),
250 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'Old Name 3')}
251 |
252 | List newList = new List {
253 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'New Name 1'),
254 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'New Name 2'),
255 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'New Name 3')}
256 |
257 | Triggers.prepare(TriggerOperation.Before_Update, oldList, newList)
258 | .beforeUpdate().bind(new MyAccountHandler()).execute();
259 | }
260 | ```
261 |
262 | ### 3.2 Test with Mockup Library
263 |
264 | The following demo is using [Apex Test Kit](https://github.com/apexfarm/ApexTestKit) as a mockup data library. The behavior will be the same as the above example, but a sophisticated mock data library can also generate mockup data with read-only fields, such as formula fields, roll-up summary fields and system fields.
265 |
266 | ```java
267 | @isTest
268 | static void test_AccountTriggerHandler_BeforeUpdate {
269 | // automatically generate fake IDs for oldList
270 | List oldList = ATK.prepare(Account.SObjectType, 3)
271 | .field(Account.Name).index('Old Name {0}')
272 | .mock().get(Account.SObjectType);
273 |
274 | // IDs in oldList will be preserved in the newList
275 | List newList = ATK.prepare(Account.SObjectType, oldList)
276 | .field(Account.Name).index('New Name {0}')
277 | .mock().get(Account.SObjectType);
278 |
279 | Triggers.prepare(TriggerOperation.Before_Update, oldList, newList)
280 | .beforeUpdate().bind(new MyAccountHandler()).execute();
281 | }
282 | ```
283 |
284 | ## 4. APIs
285 |
286 | ### 4.1 Trigger Handler Interfaces
287 |
288 | | Interface | Method to Implement |
289 | | ----------------------- | ----------------------------------------------- |
290 | | Triggers.Handler | `Boolean criteria(Triggers.Context context);` |
291 | | Triggers.BeforeInsert | `void beforeInsert(Triggers.Context context);` |
292 | | Triggers.AfterInsert | `void afterInsert(Triggers.Context context);` |
293 | | Triggers.BeforeUpdate | `void beforeUpdate(Triggers.Context context);` |
294 | | Triggers.AfterUpdate | `void afterUpdate(Triggers.Context context);` |
295 | | Triggers.BeforeDelete | `void beforeDelete(Triggers.Context context);` |
296 | | Triggers.AfterDelete | `void afterDelete(Triggers.Context context);` |
297 | | Triggers.BeforeUndelete | `void afterUndelete(Triggers.Context context);` |
298 |
299 | ### 4.2 Triggers.Context
300 |
301 | | Property/Method | Type | Description |
302 | | --------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
303 | | context.props | Triggers.Props | All properties on Trigger are exposed by this class. In addition there are frequently used helper methods and a convinient sObjectType property, in case reflection is needed . |
304 | | context.state | Map | A map provided for developers to pass any value down to other handlers. |
305 | | context.skips | Triggers.Skips | A set to store handlers to be skipped. Call the following methods to manage skips: `context.skips.add()`, `context.skips.remove()`, `context.skips.clear()` `context.skips.contains()` etc. |
306 | | context.next() | void | Call the next handler. |
307 | | context.stop() | void | Stop execute any following handlers. A bit like the the stop in process builders. |
308 |
309 | ### 4.3 Triggers.Props
310 |
311 | #### Properties
312 |
313 | | Property | Type | Description |
314 | | ------------- | ------------------ | ------------------------ |
315 | | sObjectType | SObjectType | The current SObjectType. |
316 | | isExecuting | Boolean | Trigger.isExecuting |
317 | | isBefore | Boolean | Trigger.isBefore |
318 | | isAfter | Boolean | Trigger.isAfter |
319 | | isInsert | Boolean | Trigger.isInsert |
320 | | isUpdate | Boolean | Trigger.isUpdate |
321 | | isDelete | Boolean | Trigger.isDelete |
322 | | isUndelete | Boolean | Trigger.isUndelete |
323 | | oldList | List\ | Trigger.old |
324 | | oldMap | Map\ | Trigger.oldMap |
325 | | newList | List\ | Trigger.new |
326 | | newMap | Map\ | Trigger.newMap |
327 | | operationType | TriggerOperation | Trigger.operationType |
328 | | size | Integer | Trigger.size |
329 |
330 | #### Methods
331 |
332 | **Note**: the following `isChanged` method has the same behavior has the `ISCHANGED` formula:
333 |
334 | > - This function returns `false` when evaluating any field on a newly created record.
335 | > - If a text field was previously blank, this function returns `true` when it contains any value.
336 | > - For number, percent, or currency fields, this function returns `true` when:
337 | > - The field was blank and now contains any value
338 | > - The field was zero and now is blank
339 | > - The field was zero and now contains any other value
340 |
341 | | Method | Type | Description |
342 | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- |
343 | | - `isChanged(SObjectField field1)` | Boolean | Check if any record has a field changed during an update. |
344 | | - `isChangedAny(SObjectField field1, SObjectField field2)` - `isChangedAny(SObjectField field1, SObjectField field2, SObjectField field3)` - `isChangedAny(List fields)` | Boolean | Check if any record has multiple fields changed during an update. Return `true` if any specified field is changed. |
345 | | - `isChangedAll(SObjectField field1, SObjectField field2)` - `isChangedAll(SObjectField field1, SObjectField field2, SObjectField field3)` - `isChangedAll(List fields)` | Boolean | Check if any record has multiple fields changed during an update. Return `true` only if all specified fields are changed. |
346 | | - `filterChanged(SObjectField field1)` | List\ | Filter IDs of records have a field changed during an update. |
347 | | - `filterChangedAny(SObjectField field1, SObjectField field2)` - `filterChangedAny(SObjectField field1, SObjectField field2, SObjectField field3)` - `filterChangedAny(List fields)` | List\ | Filter IDs of records have multiple fields changed during an update. Return IDs if any specified field is changed. |
348 | | - `filterChangedAll(SObjectField field1, SObjectField field2)` - `filterChangedAll(SObjectField field1, SObjectField field2, SObjectField field3)` - `filterChangedAll(List fields)` | List\ | Filter IDs of records have multiple fields changed during an update. Return IDs only if all specified fields are changed. |
349 |
350 | ## 5. License
351 |
352 | BSD 3-Clause License
353 |
--------------------------------------------------------------------------------
/apex-trigger-handler/main/default/classes/Triggers.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * BSD 3-Clause License
3 | *
4 | * Copyright (c) 2020, Jianfeng Jin
5 | * All rights reserved.
6 | *
7 | * Redistribution and use in source and binary forms, with or without
8 | * modification, are permitted provided that the following conditions are met:
9 | *
10 | * 1. Redistributions of source code must retain the above copyright notice, this
11 | * list of conditions and the following disclaimer.
12 | *
13 | * 2. Redistributions in binary form must reproduce the above copyright notice,
14 | * this list of conditions and the following disclaimer in the documentation
15 | * and/or other materials provided with the distribution.
16 | *
17 | * 3. Neither the name of the copyright holder nor the names of its
18 | * contributors may be used to endorse or promote products derived from
19 | * this software without specific prior written permission.
20 | *
21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | */
32 |
33 | public class Triggers {
34 | public static final Boolean WHEN_ALWAYS = true;
35 | public static final Skips skips = new Skips();
36 |
37 | @TestVisible
38 | private static Loader LOADER {
39 | get {
40 | if (LOADER == null) {
41 | Type LoaderType = Type.forName('TriggersLoader');
42 | if (LoaderType == null) {
43 | LOADER = new NullLoader();
44 | } else {
45 | LOADER = (Loader) LoaderType.newInstance();
46 | }
47 | }
48 | return LOADER;
49 | }
50 | set;
51 | }
52 |
53 | public static Manager prepare() {
54 | return new ManagerImpl();
55 | }
56 |
57 | @TestVisible
58 | private static Manager prepare(TriggerOperation operationType, List oldList, List newList) {
59 | Props props = new Props(operationType, oldList, newList);
60 | return new ManagerImpl(props);
61 | }
62 |
63 | @TestVisible
64 | private class ManagerImpl implements Manager {
65 | public final Props props { get; private set; }
66 | public final Context context { get; private set; }
67 |
68 | Boolean canBind = false;
69 |
70 | @TestVisible
71 | private ManagerImpl() {
72 | this(new Props());
73 | }
74 |
75 | @TestVisible
76 | private ManagerImpl(Props props) {
77 | this.props = props;
78 | this.context = new Context(this.props);
79 | }
80 |
81 | public Manager beforeInsert() {
82 | canBind = this.props.operationType == TriggerOperation.BEFORE_INSERT;
83 | return this;
84 | }
85 |
86 | public Manager afterInsert() {
87 | canBind = this.props.operationType == TriggerOperation.AFTER_INSERT;
88 | return this;
89 | }
90 |
91 | public Manager beforeUpdate() {
92 | canBind = this.props.operationType == TriggerOperation.BEFORE_UPDATE;
93 | return this;
94 | }
95 |
96 | public Manager afterUpdate() {
97 | canBind = this.props.operationType == TriggerOperation.AFTER_UPDATE;
98 | return this;
99 | }
100 |
101 | public Manager beforeDelete() {
102 | canBind = this.props.operationType == TriggerOperation.BEFORE_DELETE;
103 | return this;
104 | }
105 |
106 | public Manager afterDelete() {
107 | canBind = this.props.operationType == TriggerOperation.AFTER_DELETE;
108 | return this;
109 | }
110 |
111 | public Manager afterUndelete() {
112 | canBind = this.props.operationType == TriggerOperation.AFTER_UNDELETE;
113 | return this;
114 | }
115 |
116 | public Manager bind(Handler handler) {
117 | if (canBind) {
118 | Boolean isImplemented = false;
119 | switch on this.props.operationType {
120 | when BEFORE_INSERT {
121 | isImplemented = handler instanceof BeforeInsert;
122 | }
123 | when AFTER_INSERT {
124 | isImplemented = handler instanceof AfterInsert;
125 | }
126 | when BEFORE_UPDATE {
127 | isImplemented = handler instanceof BeforeUpdate;
128 | }
129 | when AFTER_UPDATE {
130 | isImplemented = handler instanceof AfterUpdate;
131 | }
132 | when BEFORE_DELETE {
133 | isImplemented = handler instanceof BeforeDelete;
134 | }
135 | when AFTER_DELETE {
136 | isImplemented = handler instanceof AfterDelete;
137 | }
138 | when AFTER_UNDELETE {
139 | isImplemented = handler instanceof AfterUndelete;
140 | }
141 | when else {
142 | }
143 | }
144 | if (isImplemented) {
145 | this.context.handlers.add(handler);
146 | }
147 | }
148 | return this;
149 | }
150 |
151 | public Manager load() {
152 | if (canBind) {
153 | List handlers = LOADER.load(this.props.sObjectType, this.props.operationType);
154 | for (Handler handler : handlers) {
155 | this.bind(handler);
156 | }
157 | }
158 | return this;
159 | }
160 |
161 | public Manager load(String tag) {
162 | if (canBind) {
163 | List handlers = LOADER.load(this.props.sObjectType, this.props.operationType, tag);
164 | for (Handler handler : handlers) {
165 | this.bind(handler);
166 | }
167 | }
168 | return this;
169 | }
170 |
171 | public void execute() {
172 | this.context.execute();
173 | this.props.isExecuting = false;
174 | }
175 | }
176 |
177 | public class Skips {
178 | @TestVisible
179 | private final Set skippedHandlers = new Set();
180 |
181 | public void add(type handlerType) {
182 | this.skippedHandlers.add(handlerType);
183 | }
184 |
185 | public void remove(type handlerType) {
186 | this.skippedHandlers.remove(handlerType);
187 | }
188 |
189 | public Boolean contains(type handlerType) {
190 | return this.skippedHandlers.contains(handlerType);
191 | }
192 |
193 | @TestVisible
194 | private Boolean contains(Handler handler) {
195 | return contains(getHandlerType(handler));
196 | }
197 |
198 | public void clear() {
199 | this.skippedHandlers.clear();
200 | }
201 |
202 | private Type getHandlerType(Handler handler) {
203 | String printName = String.valueOf(handler);
204 | String typeName = printName.substring(0, printName.indexOf(':'));
205 | return Type.forName(typeName);
206 | }
207 | }
208 |
209 | public class Context {
210 | public final Map state { get; private set; }
211 | public final Skips skips { get; private set; }
212 | public final Props props { get; private set; }
213 |
214 | private final List handlers = new List();
215 | private Integer currIndex = -1;
216 | private Boolean isExecutingCriteria = false;
217 |
218 | private Context(Props props) {
219 | this.props = props;
220 | this.state = new Map();
221 | this.skips = Triggers.skips;
222 | }
223 |
224 | private void reset() {
225 | this.currIndex = -1;
226 | }
227 |
228 | public void stop() {
229 | this.currIndex = this.handlers.size();
230 | }
231 |
232 | private Boolean hasNext() {
233 | return this.currIndex < this.handlers.size() - 1;
234 | }
235 |
236 | public void next() {
237 | // prevent calling context.next() in criteria phase
238 | if (!this.isExecutingCriteria) {
239 | while (hasNext()) {
240 | runNext();
241 | }
242 | }
243 | }
244 |
245 | private void execute() {
246 | reset();
247 | while (hasNext()) {
248 | runNext();
249 | }
250 | }
251 |
252 | private void runNext() {
253 | this.currIndex++;
254 | Handler handler = this.handlers[this.currIndex];
255 | this.isExecutingCriteria = true;
256 | if (!this.skips.contains(handler) && handler.criteria(this) == true) {
257 | this.isExecutingCriteria = false;
258 | switch on this.props.operationType {
259 | when BEFORE_INSERT {
260 | ((BeforeInsert) handler).beforeInsert(this);
261 | }
262 | when AFTER_INSERT {
263 | ((AfterInsert) handler).afterInsert(this);
264 | }
265 | when BEFORE_UPDATE {
266 | ((BeforeUpdate) handler).beforeUpdate(this);
267 | }
268 | when AFTER_UPDATE {
269 | ((AfterUpdate) handler).afterUpdate(this);
270 | }
271 | when BEFORE_DELETE {
272 | ((BeforeDelete) handler).beforeDelete(this);
273 | }
274 | when AFTER_DELETE {
275 | ((AfterDelete) handler).afterDelete(this);
276 | }
277 | when AFTER_UNDELETE {
278 | ((AfterUndelete) handler).afterUndelete(this);
279 | }
280 | when else {
281 | }
282 | }
283 | } else {
284 | this.isExecutingCriteria = false;
285 | }
286 | }
287 | }
288 |
289 | public class Props {
290 | // Standard Properties
291 | @TestVisible
292 | public Boolean isExecuting { get; private set; }
293 | @TestVisible
294 | public Boolean isBefore { get; private set; }
295 | @TestVisible
296 | public Boolean isAfter { get; private set; }
297 | @TestVisible
298 | public Boolean isInsert { get; private set; }
299 | @TestVisible
300 | public Boolean isUpdate { get; private set; }
301 | @TestVisible
302 | public Boolean isDelete { get; private set; }
303 | @TestVisible
304 | public Boolean isUndelete { get; private set; }
305 | @TestVisible
306 | public List oldList { get; private set; }
307 | @TestVisible
308 | public Map oldMap { get; private set; }
309 | @TestVisible
310 | public List newList { get; private set; }
311 | @TestVisible
312 | public Map newMap { get; private set; }
313 | @TestVisible
314 | public TriggerOperation operationType { get; private set; }
315 | @TestVisible
316 | public Integer size { get; private set; }
317 |
318 | // Custom Properties
319 | @TestVisible
320 | public SObjectType sObjectType { get; private set; }
321 |
322 | @TestVisible
323 | private Props() {
324 | this.isExecuting = true;
325 | this.isBefore = Trigger.isBefore;
326 | this.isAfter = Trigger.isAfter;
327 | this.isInsert = Trigger.isInsert;
328 | this.isUpdate = Trigger.isUpdate;
329 | this.isDelete = Trigger.isDelete;
330 | this.isUndelete = Trigger.isUndelete;
331 | this.oldList = Trigger.old;
332 | this.oldMap = Trigger.oldMap;
333 | this.newList = Trigger.new;
334 | this.newMap = Trigger.newMap;
335 | this.operationType = Trigger.operationType;
336 | this.size = Trigger.size;
337 | this.setSObjectType();
338 | }
339 |
340 | @TestVisible
341 | private Props(TriggerOperation operationType, List oldList, List newList) {
342 | this.isExecuting = true;
343 | this.operationType = operationType;
344 | this.isBefore = false;
345 | this.isAfter = false;
346 | this.isInsert = false;
347 | this.isUpdate = false;
348 | this.isDelete = false;
349 | this.isUndelete = false;
350 | switch on operationType {
351 | when BEFORE_INSERT {
352 | this.isBefore = true;
353 | this.isInsert = true;
354 | this.oldList = null;
355 | this.oldMap = null;
356 | this.newList = newList;
357 | this.newMap = newList != null ? new Map(newList) : null;
358 | }
359 | when AFTER_INSERT {
360 | this.isAfter = true;
361 | this.isInsert = true;
362 | this.oldList = null;
363 | this.oldMap = null;
364 | this.newList = newList;
365 | this.newMap = newList != null ? new Map(newList) : null;
366 | }
367 | when BEFORE_UPDATE {
368 | this.isBefore = true;
369 | this.isUpdate = true;
370 | this.oldList = oldList;
371 | this.oldMap = oldList != null ? new Map(oldList) : null;
372 | this.newList = newList;
373 | this.newMap = newList != null ? new Map(newList) : null;
374 | }
375 | when AFTER_UPDATE {
376 | this.isAfter = true;
377 | this.isUpdate = true;
378 | this.oldList = oldList;
379 | this.oldMap = oldList != null ? new Map(oldList) : null;
380 | this.newList = newList;
381 | this.newMap = newList != null ? new Map(newList) : null;
382 | }
383 | when BEFORE_DELETE {
384 | this.isBefore = true;
385 | this.isDelete = true;
386 | this.oldList = oldList;
387 | this.oldMap = oldList != null ? new Map(oldList) : null;
388 | this.newList = null;
389 | this.newMap = null;
390 | }
391 | when AFTER_DELETE {
392 | this.isAfter = true;
393 | this.isDelete = true;
394 | this.oldList = oldList;
395 | this.oldMap = oldList != null ? new Map(oldList) : null;
396 | this.newList = null;
397 | this.newMap = null;
398 | }
399 | when AFTER_UNDELETE {
400 | this.isAfter = true;
401 | this.isUndelete = true;
402 | this.oldList = null;
403 | this.oldMap = null;
404 | this.newList = newList;
405 | this.newMap = newList != null ? new Map(newList) : null;
406 | }
407 | when else {
408 | }
409 | }
410 | this.setSize();
411 | this.setSObjectType();
412 | }
413 |
414 | private void setSize() {
415 | this.size = 0;
416 | if (this.oldList != null) {
417 | this.size = this.oldList.size();
418 | } else if (this.newList != null) {
419 | this.size = this.newList.size();
420 | }
421 | }
422 |
423 | @TestVisible
424 | private void setSObjectType() {
425 | if (this.oldList != null) {
426 | this.sObjectType = this.oldList.getSobjectType();
427 | } else if (this.newList != null) {
428 | this.sObjectType = this.newList.getSObjectType();
429 | }
430 | }
431 |
432 | public List getValues(SObjectField field) {
433 | List values = new List();
434 | List objects = new List();
435 |
436 | if (this.isInsert || this.isUpdate || this.isUndelete) {
437 | objects = this.newList;
438 | } else if (this.isDelete) {
439 | objects = this.oldList;
440 | }
441 |
442 | for (SObject obj : objects) {
443 | values.add(obj.get(field));
444 | }
445 | return values;
446 | }
447 |
448 | @TestVisible
449 | private Boolean isChanged(Id objectId, SObjectField field) {
450 | if (this.isUpdate) {
451 | Object oldValue = this.oldMap.get(objectId).get(field);
452 | Object newValue = this.newMap.get(objectId).get(field);
453 | return oldValue != newValue;
454 | }
455 | return false;
456 | }
457 |
458 | public Boolean isChanged(SObjectField field1) {
459 | return isChangedAny(new List{ field1 });
460 | }
461 |
462 | public Boolean isChangedAny(SObjectField field1, SObjectField field2) {
463 | return isChangedAny(new List{ field1, field2 });
464 | }
465 |
466 | public Boolean isChangedAny(SObjectField field1, SObjectField field2, SObjectField field3) {
467 | return isChangedAny(new List{ field1, field2, field3 });
468 | }
469 |
470 | public Boolean isChangedAny(List fields) {
471 | return filterChangedAny(fields).size() > 0;
472 | }
473 |
474 | public Boolean isChangedAll(SObjectField field1, SObjectField field2) {
475 | return isChangedAll(new List{ field1, field2 });
476 | }
477 |
478 | public Boolean isChangedAll(SObjectField field1, SObjectField field2, SObjectField field3) {
479 | return isChangedAll(new List{ field1, field2, field3 });
480 | }
481 |
482 | public Boolean isChangedAll(List fields) {
483 | return filterChangedAll(fields).size() > 0;
484 | }
485 |
486 | public List filterChanged(SObjectField field1) {
487 | return filterChangedAny(new List{ field1 });
488 | }
489 |
490 | public List filterChangedAny(SObjectField field1, SObjectField field2) {
491 | return filterChangedAny(new List{ field1, field2 });
492 | }
493 |
494 | public List filterChangedAny(SObjectField field1, SObjectField field2, SObjectField field3) {
495 | return filterChangedAny(new List{ field1, field2, field3 });
496 | }
497 |
498 | public List filterChangedAny(List fields) {
499 | List changedIds = new List();
500 | if (this.isUpdate) {
501 | for (SObject newObj : this.newList) {
502 | Id objectId = newObj.Id;
503 | for (SObjectField field : fields) {
504 | if (isChanged(objectId, field)) {
505 | changedIds.add(objectId);
506 | break;
507 | }
508 | }
509 | }
510 | }
511 | return changedIds;
512 | }
513 |
514 | public List filterChangedAll(SObjectField field1, SObjectField field2) {
515 | return filterChangedAll(new List{ field1, field2 });
516 | }
517 |
518 | public List filterChangedAll(SObjectField field1, SObjectField field2, SObjectField field3) {
519 | return filterChangedAll(new List{ field1, field2, field3 });
520 | }
521 |
522 | public List filterChangedAll(List fields) {
523 | List changedIds = new List();
524 | if (this.isUpdate) {
525 | for (SObject newObj : this.newList) {
526 | Id objectId = newObj.Id;
527 | changedIds.add(objectId);
528 | for (SObjectField field : fields) {
529 | if (!isChanged(objectId, field)) {
530 | changedIds.remove(changedIds.size() - 1);
531 | break;
532 | }
533 | }
534 | }
535 | }
536 | return changedIds;
537 | }
538 | }
539 |
540 | @TestVisible
541 | private class NullLoader implements Loader {
542 | public List load(SObjectType sObjectType, TriggerOperation triggerEvent) {
543 | return new List();
544 | }
545 |
546 | public List load(SObjectType sObjectType, TriggerOperation triggerEvent, String tag) {
547 | return new List();
548 | }
549 | }
550 |
551 | //===================
552 | // #region interfaces
553 | public interface Loader {
554 | List load(SObjectType sObjectType, TriggerOperation triggerEvent);
555 | List load(SObjectType sObjectType, TriggerOperation triggerEvent, String tag);
556 | }
557 |
558 | public interface Manager {
559 | Manager beforeInsert();
560 | Manager afterInsert();
561 | Manager beforeUpdate();
562 | Manager afterUpdate();
563 | Manager beforeDelete();
564 | Manager afterDelete();
565 | Manager afterUndelete();
566 | Manager bind(Handler handler);
567 | Manager load();
568 | Manager load(String tag);
569 | void execute();
570 | }
571 |
572 | public interface Handler {
573 | Boolean criteria(Context context);
574 | }
575 |
576 | public interface BeforeInsert {
577 | void beforeInsert(Context context);
578 | }
579 |
580 | public interface AfterInsert {
581 | void afterInsert(Context context);
582 | }
583 |
584 | public interface BeforeUpdate {
585 | void beforeUpdate(Context context);
586 | }
587 |
588 | public interface AfterUpdate {
589 | void afterUpdate(Context context);
590 | }
591 |
592 | public interface BeforeDelete {
593 | void beforeDelete(Context context);
594 | }
595 |
596 | public interface AfterDelete {
597 | void afterDelete(Context context);
598 | }
599 |
600 | public interface AfterUndelete {
601 | void afterUndelete(Context context);
602 | }
603 | // #endregion
604 | //===================
605 | }
606 |
--------------------------------------------------------------------------------
/apex-trigger-handler/main/default/classes/Triggers.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 57.0
4 | Active
5 |
--------------------------------------------------------------------------------
/apex-trigger-handler/main/default/classes/TriggersTest.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * BSD 3-Clause License
3 | *
4 | * Copyright (c) 2020, Jianfeng Jin
5 | * All rights reserved.
6 | *
7 | * Redistribution and use in source and binary forms, with or without
8 | * modification, are permitted provided that the following conditions are met:
9 | *
10 | * 1. Redistributions of source code must retain the above copyright notice, this
11 | * list of conditions and the following disclaimer.
12 | *
13 | * 2. Redistributions in binary form must reproduce the above copyright notice,
14 | * this list of conditions and the following disclaimer in the documentation
15 | * and/or other materials provided with the distribution.
16 | *
17 | * 3. Neither the name of the copyright holder nor the names of its
18 | * contributors may be used to endorse or promote products derived from
19 | * this software without specific prior written permission.
20 | *
21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | */
32 |
33 | @IsTest
34 | public class TriggersTest implements Triggers.Handler, Triggers.BeforeInsert {
35 | public static String getFakeId(Schema.SObjectType objectType, Integer index) {
36 | return objectType.getDescribe().getKeyPrefix()
37 | + '000zzzz' // start from a large Id to avoid confliction during unit test.
38 | + String.valueOf(index).leftPad(5, '0');
39 | }
40 |
41 | static List createAccounts() {
42 | return new List {
43 | new Account(Id = getFakeId(Account.SObjectType, 1), Name = 'Account 1', Description = 'Account 1', BillingCity = 'New York'),
44 | new Account(Id = getFakeId(Account.SObjectType, 2), Name = 'Account 2', Description = 'Account 2', BillingCity = 'New York'),
45 | new Account(Id = getFakeId(Account.SObjectType, 3), Name = 'Account 3', Description = 'Account 3', BillingCity = 'New York')
46 | };
47 | }
48 |
49 | @IsTest
50 | static void testSkips_getHandlerName_ByType() {
51 | Triggers.Skips skips = new Triggers.Skips();
52 | skips.add(String.class);
53 | Assert.areEqual(true, skips.contains(String.class));
54 |
55 | skips.remove(String.class);
56 | Assert.areEqual(0, skips.skippedHandlers.size());
57 |
58 | skips.add(TriggersTest.TriggersTest.class);
59 | Assert.areEqual(true, skips.contains(TriggersTest.class));
60 |
61 | skips.clear();
62 | Assert.areEqual(0, skips.skippedHandlers.size());
63 |
64 | skips.add(TriggersTest.class);
65 | Assert.areEqual(true, skips.contains(new TriggersTest()));
66 | }
67 |
68 | // ====================
69 | // #region Test Binding
70 | @IsTest
71 | static void testBinding_BaseLine() {
72 | Triggers.prepare()
73 | .beforeInsert()
74 | .bind(new MainHandler())
75 | .beforeUpdate()
76 | .bind(new MainHandler())
77 | .beforeDelete()
78 | .bind(new MainHandler())
79 | .execute();
80 |
81 | Triggers.prepare()
82 | .afterInsert()
83 | .bind(new MainHandler())
84 | .afterUpdate()
85 | .bind(new MainHandler())
86 | .afterDelete()
87 | .bind(new MainHandler())
88 | .afterUndelete()
89 | .bind(new MainHandler())
90 | .execute();
91 | }
92 |
93 | @IsTest
94 | static void testBinding_NullLoader() {
95 | Triggers.Loader loader = Triggers.LOADER;
96 |
97 | Triggers.LOADER = new Triggers.NullLoader();
98 | Triggers.prepare(TriggerOperation.BEFORE_INSERT, null, null)
99 | .beforeInsert()
100 | .load()
101 | .load('tag1')
102 | .execute();
103 | }
104 |
105 | @IsTest
106 | static void testBinding_Stop_BeforeInsert() {
107 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.BEFORE_INSERT, null, null);
108 | triggerManager.beforeInsert()
109 | .bind(new FirstHandler())
110 | .bind(new MainHandler())
111 | .bind(new MainHandler())
112 | .bind(new StopHandler())
113 | .bind(new MainHandler())
114 | .bind(new LastHandler())
115 | .execute();
116 |
117 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
118 | }
119 |
120 | @IsTest
121 | static void testBinding_Stop_AfterInsert() {
122 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.AFTER_INSERT, null, null);
123 | triggerManager.afterInsert()
124 | .bind(new FirstHandler())
125 | .bind(new MainHandler())
126 | .bind(new MainHandler())
127 | .bind(new StopHandler())
128 | .bind(new MainHandler())
129 | .bind(new LastHandler())
130 | .execute();
131 |
132 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
133 | }
134 |
135 | @IsTest
136 | static void testBinding_Inactive_BeforeInsert() {
137 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.BEFORE_INSERT, null, null);
138 | triggerManager.beforeInsert()
139 | .bind(new FirstHandler())
140 | .bind(new MainHandler())
141 | .bind(new InactiveHandler())
142 | .bind(new MainHandler())
143 | .bind(new LastHandler())
144 | .execute();
145 |
146 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
147 | }
148 |
149 | @IsTest
150 | static void testBinding_Skip_Remove() {
151 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.BEFORE_INSERT, null, null);
152 | triggerManager.beforeInsert()
153 | .bind(new FirstHandler())
154 | .bind(new MainHandler())
155 | .bind(new AddSkippedHandler())
156 | .bind(new TriggersTest())
157 | .bind(new RemoveSkippedHandler())
158 | .bind(new TriggersTest())
159 | .bind(new LastHandler())
160 | .execute();
161 |
162 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
163 | }
164 |
165 | @IsTest
166 | static void testBinding_Skip_Clear() {
167 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.BEFORE_INSERT, null, null);
168 | triggerManager
169 | .beforeInsert()
170 | .bind(new FirstHandler())
171 | .bind(new MainHandler())
172 | .bind(new AddSkippedHandler())
173 | .bind(new TriggersTest())
174 | .bind(new ClearSkippedHandler())
175 | .bind(new TriggersTest())
176 | .bind(new LastHandler())
177 | .execute();
178 |
179 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
180 | }
181 | // #endregion
182 | // ====================
183 |
184 | // ==================
185 | // #region Test State
186 | @IsTest
187 | static void testBinding_State_BeforeInsert() {
188 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(
189 | TriggerOperation.BEFORE_INSERT, new List{}, new List());
190 | triggerManager
191 | .beforeInsert()
192 | .bind(new FirstHandler())
193 | .bind(new MainHandler())
194 | .bind(new MainHandler())
195 | .bind(new LastHandler())
196 | .execute();
197 |
198 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
199 | }
200 |
201 | @IsTest
202 | static void testBinding_State_AfterInsert() {
203 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.AFTER_INSERT, null, null);
204 | triggerManager
205 | .afterInsert()
206 | .bind(new FirstHandler())
207 | .bind(new MainHandler())
208 | .bind(new MainHandler())
209 | .bind(new LastHandler())
210 | .execute();
211 |
212 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
213 | }
214 |
215 | @IsTest
216 | static void testBinding_State_BeforeUpdate() {
217 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(
218 | TriggerOperation.BEFORE_UPDATE, new List(), new List());
219 | triggerManager
220 | .beforeUpdate()
221 | .bind(new FirstHandler())
222 | .bind(new MainHandler())
223 | .bind(new MainHandler())
224 | .bind(new LastHandler())
225 | .execute();
226 |
227 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
228 | }
229 |
230 | @IsTest
231 | static void testBinding_State_AfterUpdate() {
232 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(
233 | TriggerOperation.AFTER_UPDATE, new List (), new List ());
234 | triggerManager
235 | .afterUpdate()
236 | .bind(new FirstHandler())
237 | .bind(new MainHandler())
238 | .bind(new MainHandler())
239 | .bind(new LastHandler())
240 | .execute();
241 |
242 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
243 | }
244 |
245 | @IsTest
246 | static void testBinding_State_BeforeDelete() {
247 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.BEFORE_DELETE, null, null);
248 | triggerManager
249 | .beforeDelete()
250 | .bind(new FirstHandler())
251 | .bind(new MainHandler())
252 | .bind(new MainHandler())
253 | .bind(new LastHandler())
254 | .execute();
255 |
256 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
257 | }
258 |
259 | @IsTest
260 | static void testBinding_State_AfterDelete() {
261 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.AFTER_DELETE, null, null);
262 | triggerManager
263 | .afterDelete()
264 | .bind(new FirstHandler())
265 | .bind(new MainHandler())
266 | .bind(new MainHandler())
267 | .bind(new LastHandler())
268 | .execute();
269 |
270 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
271 | }
272 |
273 | @IsTest
274 | static void testBinding_State_AfterUndelete() {
275 | Triggers.ManagerImpl triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.AFTER_UNDELETE, null, null);
276 | triggerManager
277 | .afterUndelete()
278 | .bind(new FirstHandler())
279 | .bind(new MainHandler())
280 | .bind(new MainHandler())
281 | .bind(new LastHandler())
282 | .execute();
283 |
284 | Assert.areEqual(4, triggerManager.context.state.get('counter'));
285 | }
286 |
287 | @IsTest
288 | static void testBinding_Mock() {
289 | List oldList = new List {
290 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'Old Name 1'),
291 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'Old Name 2'),
292 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'Old Name 3')};
293 |
294 | List newList = new List {
295 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'New Name 1'),
296 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'New Name 2'),
297 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'New Name 3')};
298 |
299 | Triggers.prepare(TriggerOperation.BEFORE_INSERT, oldList, newList)
300 | .afterUpdate()
301 | .bind(new AccountNameChangedHandler())
302 | .execute();
303 |
304 | Triggers.prepare(TriggerOperation.AFTER_INSERT, oldList, newList)
305 | .afterUpdate()
306 | .bind(new AccountNameChangedHandler())
307 | .execute();
308 |
309 | Triggers.prepare(TriggerOperation.BEFORE_UPDATE, oldList, newList)
310 | .afterUpdate()
311 | .bind(new AccountNameChangedHandler())
312 | .execute();
313 |
314 | Triggers.prepare(TriggerOperation.AFTER_UPDATE, oldList, newList)
315 | .afterUpdate()
316 | .bind(new AccountNameChangedHandler())
317 | .execute();
318 |
319 | Triggers.prepare(TriggerOperation.BEFORE_DELETE, oldList, newList)
320 | .afterUpdate()
321 | .bind(new AccountNameChangedHandler())
322 | .execute();
323 |
324 | Triggers.prepare(TriggerOperation.AFTER_DELETE, oldList, newList)
325 | .afterUpdate()
326 | .bind(new AccountNameChangedHandler())
327 | .execute();
328 |
329 | Triggers.prepare(TriggerOperation.AFTER_UNDELETE, oldList, newList)
330 | .afterUpdate()
331 | .bind(new AccountNameChangedHandler())
332 | .execute();
333 | }
334 | // #endregion
335 | // ==================
336 |
337 | // ===================
338 | // #region Test Helper
339 | @IsTest
340 | static void testProps_IsChanged_Negative() {
341 | Triggers.Props props = new Triggers.Props();
342 | props.isInsert = true;
343 | props.isUpdate = false;
344 | props.isDelete = false;
345 | props.isUndelete = false;
346 |
347 | List accounts = createAccounts();
348 | props.newList = accounts;
349 | props.newMap = new Map(accounts);
350 |
351 | Boolean isChanged = props.isChanged(accounts[0].Id, Account.Name);
352 |
353 | Assert.areEqual(false, isChanged);
354 | }
355 |
356 | @IsTest
357 | static void testProps_GetValues_Undelete() {
358 | Triggers.Props props = new Triggers.Props();
359 | props.isInsert = false;
360 | props.isUpdate = false;
361 | props.isDelete = false;
362 | props.isUndelete = true;
363 | props.newList = new List {
364 | new Account(Name = 'Account 1'),
365 | new Account(Name = 'Account 2'),
366 | new Account(Name = 'Account 3')
367 | };
368 |
369 | List names = props.getValues(Account.Name);
370 | Assert.areEqual(3, names.size());
371 | Assert.areEqual('Account 1', (String)names[0]);
372 | Assert.areEqual('Account 2', (String)names[1]);
373 | Assert.areEqual('Account 3', (String)names[2]);
374 | }
375 |
376 | @IsTest
377 | static void testProps_GetValues_Delete() {
378 | Triggers.Props props = new Triggers.Props();
379 | props.isInsert = false;
380 | props.isUpdate = false;
381 | props.isDelete = true;
382 | props.isUndelete = false;
383 | props.oldList = new List {
384 | new Account(Name = 'Account 1'),
385 | new Account(Name = 'Account 2'),
386 | new Account(Name = 'Account 3')
387 | };
388 |
389 | List names = props.getValues(Account.Name);
390 | Assert.areEqual(3, names.size());
391 | Assert.areEqual('Account 1', (String)names[0]);
392 | Assert.areEqual('Account 2', (String)names[1]);
393 | Assert.areEqual('Account 3', (String)names[2]);
394 | }
395 |
396 | @IsTest
397 | static void testProps_FilterChanged_NoChange() {
398 | Triggers.Props props = new Triggers.Props();
399 | props.isInsert = false;
400 | props.isUpdate = true;
401 | props.isDelete = false;
402 | props.isUndelete = false;
403 |
404 | List accounts = createAccounts();
405 | props.oldList = accounts;
406 | props.oldMap = new Map(accounts);
407 | props.newList = accounts;
408 | props.newMap = new Map(accounts);
409 |
410 | List changedIds = props.filterChanged(Account.Name);
411 | Boolean isChanged = props.isChanged(Account.Name);
412 |
413 | Assert.areEqual(0, changedIds.size());
414 | Assert.areEqual(false, isChanged);
415 | }
416 |
417 | @IsTest
418 | static void testProps_FilterChanged() {
419 | Triggers.Props props = new Triggers.Props();
420 | props.isInsert = false;
421 | props.isUpdate = true;
422 | props.isDelete = false;
423 | props.isUndelete = false;
424 | List accounts = createAccounts();
425 | props.oldList = accounts;
426 | props.oldMap = new Map(accounts);
427 | List newAccounts = accounts.deepClone();
428 | newAccounts[0].Id = accounts[0].Id;
429 | newAccounts[1].Id = accounts[1].Id;
430 | newAccounts[1].Name = 'Account 4';
431 | newAccounts[2].Id = accounts[2].Id;
432 | newAccounts[2].Description = 'Account 5';
433 | props.newList = newAccounts;
434 | props.newMap = new Map(newAccounts);
435 |
436 | List changedIds = props.filterChanged(Account.Name);
437 | Boolean isChanged = props.isChanged(Account.Name);
438 |
439 | Assert.areEqual(1, changedIds.size());
440 | Assert.areEqual(true, isChanged);
441 | }
442 |
443 |
444 | @IsTest
445 | static void testProps_FilterChangedAny_X2_NoChange() {
446 | Triggers.Props props = new Triggers.Props();
447 | props.isInsert = false;
448 | props.isUpdate = true;
449 | props.isDelete = false;
450 | props.isUndelete = false;
451 |
452 | List accounts = createAccounts();
453 | props.oldList = accounts;
454 | props.oldMap = new Map(accounts);
455 | props.newList = accounts;
456 | props.newMap = new Map(accounts);
457 |
458 | List changedIds = props.filterChangedAny(Account.Name, Account.Description);
459 | Boolean isChanged = props.isChangedAny(Account.Name, Account.Description);
460 |
461 | Assert.areEqual(0, changedIds.size());
462 | Assert.areEqual(false, isChanged);
463 | }
464 |
465 | @IsTest
466 | static void testProps_FilterChangedAny_X2() {
467 | Triggers.Props props = new Triggers.Props();
468 | props.isInsert = false;
469 | props.isUpdate = true;
470 | props.isDelete = false;
471 | props.isUndelete = false;
472 | List accounts = createAccounts();
473 | props.oldList = accounts;
474 | props.oldMap = new Map(accounts);
475 | List newAccounts = accounts.deepClone();
476 | newAccounts[0].Id = accounts[0].Id;
477 | newAccounts[1].Id = accounts[1].Id;
478 | newAccounts[1].Name = 'Account 4';
479 | newAccounts[2].Id = accounts[2].Id;
480 | newAccounts[2].Description = 'Account 5';
481 | props.newList = newAccounts;
482 | props.newMap = new Map(newAccounts);
483 |
484 | List changedIds = props.filterChangedAny(Account.Name, Account.Description);
485 | Boolean isChanged = props.isChangedAny(Account.Name, Account.Description);
486 |
487 | Assert.areEqual(2, changedIds.size());
488 | Assert.areEqual(true, isChanged);
489 | }
490 |
491 | @IsTest
492 | static void testProps_FilterChangedAny_X3() {
493 | Triggers.Props props = new Triggers.Props();
494 | props.isInsert = false;
495 | props.isUpdate = true;
496 | props.isDelete = false;
497 | props.isUndelete = false;
498 | List accounts = createAccounts();
499 | props.oldList = accounts;
500 | props.oldMap = new Map(accounts);
501 | List newAccounts = accounts.deepClone();
502 | newAccounts[0].Id = accounts[0].Id;
503 | newAccounts[0].BillingCity = 'Account 4';
504 | newAccounts[1].Id = accounts[1].Id;
505 | newAccounts[1].Name = 'Account 4';
506 | newAccounts[2].Id = accounts[2].Id;
507 | newAccounts[2].Description = 'Account 5';
508 | props.newList = newAccounts;
509 | props.newMap = new Map(newAccounts);
510 |
511 | List changedIds = props.filterChangedAny(Account.Name, Account.Description, Account.BillingCity);
512 | Boolean isChanged = props.isChangedAny(Account.Name, Account.Description, Account.BillingCity);
513 |
514 | Assert.areEqual(3, changedIds.size());
515 | Assert.areEqual(true, isChanged);
516 | }
517 |
518 | @IsTest
519 | static void testProps_FilterChangedAll_X2_NoChange() {
520 | Triggers.Props props = new Triggers.Props();
521 | props.isInsert = false;
522 | props.isUpdate = true;
523 | props.isDelete = false;
524 | props.isUndelete = false;
525 |
526 | List accounts = createAccounts();
527 | props.oldList = accounts;
528 | props.oldMap = new Map(accounts);
529 | props.newList = accounts;
530 | props.newMap = new Map(accounts);
531 |
532 | List changedIds = props.filterChangedAll(Account.Name, Account.Description);
533 | Boolean isChanged = props.isChangedAll(Account.Name, Account.Description);
534 |
535 | Assert.areEqual(0, changedIds.size());
536 | Assert.areEqual(false, isChanged);
537 | }
538 |
539 | @IsTest
540 | static void testProps_FilterChangedAll_X2() {
541 | Triggers.Props props = new Triggers.Props();
542 | props.isInsert = false;
543 | props.isUpdate = true;
544 | props.isDelete = false;
545 | props.isUndelete = false;
546 | List accounts = createAccounts();
547 | props.oldList = accounts;
548 | props.oldMap = new Map(accounts);
549 | List newAccounts = accounts.deepClone();
550 | newAccounts[0].Id = accounts[0].Id;
551 | newAccounts[0].Name = 'Account 6';
552 | newAccounts[0].Description = 'Account 6';
553 | newAccounts[1].Id = accounts[1].Id;
554 | newAccounts[1].Name = 'Account 4';
555 | newAccounts[2].Id = accounts[2].Id;
556 | newAccounts[2].Description = 'Account 5';
557 | props.newList = newAccounts;
558 | props.newMap = new Map(newAccounts);
559 |
560 | List changedIds = props.filterChangedAll(Account.Name, Account.Description);
561 | Boolean isChanged = props.isChangedAll(Account.Name, Account.Description);
562 |
563 | Assert.areEqual(1, changedIds.size());
564 | Assert.areEqual(true, isChanged);
565 | }
566 |
567 | @IsTest
568 | static void testProps_sObjectType() {
569 | Triggers.Props props = new Triggers.Props();
570 | props.isInsert = false;
571 | props.isUpdate = true;
572 | props.isDelete = false;
573 | props.isUndelete = false;
574 | List accounts = createAccounts();
575 | props.oldList = accounts;
576 | props.oldMap = new Map(accounts);
577 | List newAccounts = accounts.deepClone();
578 | newAccounts[0].Id = accounts[0].Id;
579 | newAccounts[0].Name = 'Account 6';
580 | newAccounts[0].Description = 'Account 6';
581 | newAccounts[1].Id = accounts[1].Id;
582 | newAccounts[1].Name = 'Account 4';
583 | newAccounts[2].Id = accounts[2].Id;
584 | newAccounts[2].Description = 'Account 5';
585 | props.newList = newAccounts;
586 | props.newMap = new Map(newAccounts);
587 |
588 | Assert.areEqual(null, props.sObjectType);
589 | props.setSObjectType();
590 | Assert.areEqual(Account.SObjectType, props.sObjectType);
591 | }
592 |
593 | @IsTest
594 | static void testProps_FilterChangedAll_X3() {
595 | Triggers.Props props = new Triggers.Props();
596 | props.isInsert = false;
597 | props.isUpdate = true;
598 | props.isDelete = false;
599 | props.isUndelete = false;
600 | List accounts = createAccounts();
601 | props.oldList = accounts;
602 | props.oldMap = new Map(accounts);
603 | List newAccounts = accounts.deepClone();
604 | newAccounts[0].Id = accounts[0].Id;
605 | newAccounts[0].Name = 'Account 6';
606 | newAccounts[0].Description = 'Account 6';
607 | newAccounts[0].BillingCity = 'Dalian';
608 | newAccounts[1].Id = accounts[1].Id;
609 | newAccounts[1].Name = 'Account 4';
610 | newAccounts[1].Description = 'Account 4';
611 | newAccounts[2].Id = accounts[2].Id;
612 | newAccounts[2].Description = 'Account 5';
613 | newAccounts[2].BillingCity = 'Dalian';
614 | props.newList = newAccounts;
615 | props.newMap = new Map(newAccounts);
616 |
617 | List changedIds = props.filterChangedAll(Account.Name, Account.Description, Account.BillingCity);
618 | Boolean isChanged = props.isChangedAll(Account.Name, Account.Description, Account.BillingCity);
619 |
620 | Assert.areEqual(1, changedIds.size());
621 | Assert.areEqual(true, isChanged);
622 | }
623 | // #endregion
624 | // ===================
625 |
626 | // =====================
627 | // #region Handler Impls
628 | // TriggersTest is treated as a handler to be skipped, because inner class cannot be reflected
629 | // from an instance back to type properly.
630 | public Boolean criteria(Triggers.Context context) {
631 | return Triggers.WHEN_ALWAYS;
632 | }
633 |
634 | public void beforeInsert(Triggers.Context context) {
635 | context.state.put('counter', (Integer)context.state.get('counter') + 1);
636 | context.next();
637 | context.state.put('counter', (Integer)context.state.get('counter') + 1);
638 | }
639 |
640 | public class FirstHandler implements Triggers.Handler, Triggers.BeforeInsert, Triggers.AfterInsert,
641 | Triggers.BeforeUpdate, Triggers.AfterUpdate, Triggers.BeforeDelete, Triggers.AfterDelete,
642 | Triggers.AfterUndelete {
643 | public Boolean criteria(Triggers.Context context) {
644 | context.next(); // negative case, shouldn't do this
645 | return Triggers.WHEN_ALWAYS;
646 | }
647 |
648 | private void then(Triggers.Context context) {
649 | if (context.state.get('counter') == null) {
650 | context.state.put('counter', 0);
651 | }
652 | Assert.areEqual(0, context.state.get('counter'));
653 | context.next();
654 | Assert.areEqual(4, context.state.get('counter'));
655 | }
656 |
657 | public void beforeInsert(Triggers.Context context) {
658 | then(context);
659 | }
660 |
661 | public void afterInsert(Triggers.Context context) {
662 | then(context);
663 | }
664 |
665 | public void beforeUpdate(Triggers.Context context) {
666 | then(context);
667 | }
668 |
669 | public void afterUpdate(Triggers.Context context) {
670 | then(context);
671 | }
672 |
673 | public void beforeDelete(Triggers.Context context) {
674 | then(context);
675 | }
676 |
677 | public void afterDelete(Triggers.Context context) {
678 | then(context);
679 | }
680 |
681 | public void afterUndelete(Triggers.Context context) {
682 | then(context);
683 | }
684 | }
685 |
686 | public class MainHandler implements Triggers.Handler, Triggers.BeforeInsert, Triggers.AfterInsert,
687 | Triggers.BeforeUpdate, Triggers.AfterUpdate, Triggers.BeforeDelete, Triggers.AfterDelete,
688 | Triggers.AfterUndelete {
689 | public Boolean criteria(Triggers.Context context) {
690 | context.next(); // shouldn't work in when method
691 | return Triggers.WHEN_ALWAYS;
692 | }
693 |
694 | private void then(Triggers.Context context) {
695 | context.state.put('counter', (Integer)context.state.get('counter') + 1);
696 | context.next();
697 | context.state.put('counter', (Integer)context.state.get('counter') + 1);
698 | }
699 |
700 | public void beforeInsert(Triggers.Context context) {
701 | then(context);
702 | }
703 |
704 | public void afterInsert(Triggers.Context context) {
705 | then(context);
706 | }
707 |
708 | public void beforeUpdate(Triggers.Context context) {
709 | then(context);
710 | }
711 |
712 | public void afterUpdate(Triggers.Context context) {
713 | then(context);
714 | }
715 |
716 | public void beforeDelete(Triggers.Context context) {
717 | then(context);
718 | }
719 |
720 | public void afterDelete(Triggers.Context context) {
721 | then(context);
722 | }
723 |
724 | public void afterUndelete(Triggers.Context context) {
725 | then(context);
726 | }
727 | }
728 |
729 | public class StopHandler implements Triggers.Handler, Triggers.BeforeInsert, Triggers.AfterInsert {
730 | public Boolean criteria(Triggers.Context context) {
731 | context.next(); // shouldn't work in when method
732 | return Triggers.WHEN_ALWAYS;
733 | }
734 |
735 | private void then(Triggers.Context context) {
736 | context.stop();
737 | }
738 |
739 | public void beforeInsert(Triggers.Context context) {
740 | then(context);
741 | }
742 |
743 | public void afterInsert(Triggers.Context context) {
744 | then(context);
745 | }
746 | }
747 |
748 | public class InactiveHandler implements Triggers.Handler, Triggers.BeforeInsert {
749 | public Boolean criteria(Triggers.Context context) {
750 | context.next(); // negative test, shouldn't work in when method
751 | context.next();
752 | context.next();
753 | return !Triggers.WHEN_ALWAYS;
754 | }
755 |
756 | private void then(Triggers.Context context) {
757 | context.state.put('counter', (Integer)context.state.get('counter') + 1);
758 | context.next();
759 | context.state.put('counter', (Integer)context.state.get('counter') + 1);
760 | }
761 |
762 | public void beforeInsert(Triggers.Context context) {
763 | then(context);
764 | }
765 | }
766 |
767 | public class AddSkippedHandler implements Triggers.Handler, Triggers.BeforeInsert {
768 | public Boolean criteria(Triggers.Context context) {
769 | return Triggers.WHEN_ALWAYS;
770 | }
771 |
772 | private void then(Triggers.Context context) {
773 | context.skips.add(TriggersTest.class);
774 | context.next();
775 | }
776 |
777 | public void beforeInsert(Triggers.Context context) {
778 | then(context);
779 | }
780 | }
781 |
782 | public class RemoveSkippedHandler implements Triggers.Handler, Triggers.BeforeInsert {
783 | public Boolean criteria(Triggers.Context context) {
784 | return Triggers.WHEN_ALWAYS;
785 | }
786 |
787 | private void then(Triggers.Context context) {
788 | if (context.skips.contains(TriggersTest.class)) {
789 | context.skips.remove(TriggersTest.class);
790 | }
791 | context.next();
792 | }
793 |
794 | public void beforeInsert(Triggers.Context context) {
795 | then(context);
796 | }
797 | }
798 |
799 | public class ClearSkippedHandler implements Triggers.Handler, Triggers.BeforeInsert {
800 | public Boolean criteria(Triggers.Context context) {
801 | return Triggers.WHEN_ALWAYS;
802 | }
803 |
804 | private void then(Triggers.Context context) {
805 | context.skips.clear();
806 | context.next();
807 | }
808 |
809 | public void beforeInsert(Triggers.Context context) {
810 | then(context);
811 | }
812 | }
813 |
814 | public class LastHandler implements Triggers.Handler, Triggers.BeforeInsert, Triggers.AfterInsert,
815 | Triggers.BeforeUpdate, Triggers.AfterUpdate, Triggers.BeforeDelete, Triggers.AfterDelete,
816 | Triggers.AfterUndelete {
817 | public Boolean criteria(Triggers.Context context) {
818 | context.next(); // shouldn't work in when method
819 | return Triggers.WHEN_ALWAYS;
820 | }
821 |
822 | private void then(Triggers.Context context) {
823 | Assert.areEqual(2, context.state.get('counter'));
824 | context.next();
825 | Assert.areEqual(2, context.state.get('counter'));
826 | }
827 |
828 | public void beforeInsert(Triggers.Context context) {
829 | then(context);
830 | }
831 |
832 | public void afterInsert(Triggers.Context context) {
833 | then(context);
834 | }
835 |
836 | public void beforeUpdate(Triggers.Context context) {
837 | then(context);
838 | }
839 |
840 | public void afterUpdate(Triggers.Context context) {
841 | then(context);
842 | }
843 |
844 | public void beforeDelete(Triggers.Context context) {
845 | then(context);
846 | }
847 |
848 | public void afterDelete(Triggers.Context context) {
849 | then(context);
850 | }
851 |
852 | public void afterUndelete(Triggers.Context context) {
853 | then(context);
854 | }
855 | }
856 |
857 | public class AccountNameChangedHandler implements Triggers.Handler, Triggers.BeforeInsert,
858 | Triggers.AfterInsert, Triggers.BeforeUpdate, Triggers.AfterUpdate, Triggers.BeforeDelete,
859 | Triggers.AfterDelete, Triggers.AfterUndelete {
860 | public Boolean criteria(Triggers.Context context) {
861 | return Triggers.WHEN_ALWAYS;
862 | }
863 |
864 | public void beforeInsert(Triggers.Context context) {
865 | Assert.areEqual(null, context.props.oldList);
866 | Assert.areEqual(null, context.props.oldMap);
867 | Assert.areEqual(3, context.props.newList.size());
868 | Assert.areEqual(3, context.props.newMap.size());
869 | }
870 |
871 | public void afterInsert(Triggers.Context context) {
872 | Assert.areEqual(null, context.props.oldList);
873 | Assert.areEqual(null, context.props.oldMap);
874 | Assert.areEqual(3, context.props.newList.size());
875 | Assert.areEqual(3, context.props.newMap.size());
876 | }
877 |
878 | public void beforeUpdate(Triggers.Context context) {
879 | Assert.areEqual(3, context.props.oldList.size());
880 | Assert.areEqual(3, context.props.oldMap.size());
881 | Assert.areEqual(3, context.props.newList.size());
882 | Assert.areEqual(3, context.props.newMap.size());
883 |
884 | Assert.areEqual(3, context.props.filterChanged(Account.Name).size());
885 | Assert.areEqual(true, context.props.isChanged(Account.Name));
886 | }
887 |
888 | public void afterUpdate(Triggers.Context context) {
889 | Assert.areEqual(3, context.props.oldList.size());
890 | Assert.areEqual(3, context.props.oldMap.size());
891 | Assert.areEqual(3, context.props.newList.size());
892 | Assert.areEqual(3, context.props.newMap.size());
893 |
894 | Assert.areEqual(3, context.props.filterChanged(Account.Name).size());
895 | Assert.areEqual(true, context.props.isChanged(Account.Name));
896 | }
897 |
898 | public void beforeDelete(Triggers.Context context) {
899 | Assert.areEqual(3, context.props.oldList.size());
900 | Assert.areEqual(3, context.props.oldMap.size());
901 | Assert.areEqual(null, context.props.newList);
902 | Assert.areEqual(null, context.props.newMap);
903 |
904 | }
905 |
906 | public void afterDelete(Triggers.Context context) {
907 | Assert.areEqual(3, context.props.oldList.size());
908 | Assert.areEqual(3, context.props.oldMap.size());
909 | Assert.areEqual(null, context.props.newList);
910 | Assert.areEqual(null, context.props.newMap);
911 | }
912 |
913 | public void afterUndelete(Triggers.Context context) {
914 | Assert.areEqual(null, context.props.oldList);
915 | Assert.areEqual(null, context.props.oldMap);
916 | Assert.areEqual(3, context.props.newList.size());
917 | Assert.areEqual(3, context.props.newMap.size());
918 | }
919 | }
920 | // #endregion
921 | // =====================
922 | }
--------------------------------------------------------------------------------
/apex-trigger-handler/main/default/classes/TriggersTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 57.0
4 | Active
5 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/classes/TriggersLoader.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * BSD 3-Clause License
3 | *
4 | * Copyright (c) 2020, Jianfeng Jin
5 | * All rights reserved.
6 | *
7 | * Redistribution and use in source and binary forms, with or without
8 | * modification, are permitted provided that the following conditions are met:
9 | *
10 | * 1. Redistributions of source code must retain the above copyright notice, this
11 | * list of conditions and the following disclaimer.
12 | *
13 | * 2. Redistributions in binary form must reproduce the above copyright notice,
14 | * this list of conditions and the following disclaimer in the documentation
15 | * and/or other materials provided with the distribution.
16 | *
17 | * 3. Neither the name of the copyright holder nor the names of its
18 | * contributors may be used to endorse or promote products derived from
19 | * this software without specific prior written permission.
20 | *
21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | */
32 |
33 | public class TriggersLoader implements Triggers.Loader {
34 | @TestVisible
35 | private static List settings {
36 | get {
37 | if (settings == null) {
38 | settings = [
39 | SELECT Id, Label, SObject__c, Trigger_Event__c, Handler_Class__c, Execution_Order__c, Tag__c
40 | FROM Apex_Trigger_Handler_Setting__mdt
41 | WHERE Active__c = TRUE
42 | ORDER BY SObject__c, Trigger_Event__c, Execution_Order__c
43 | ];
44 | }
45 | return settings;
46 | }
47 | set;
48 | }
49 |
50 | private static Map>> handlerSettings {
51 | get {
52 | if (handlerSettings == null) {
53 | handlerSettings = new Map>>();
54 | SObjectType preObjectType = null;
55 | TriggerOperation preTriggerEvent = null;
56 | for (Apex_Trigger_Handler_Setting__mdt setting : settings) {
57 | SObjectType objectType = Schema.getGlobalDescribe().get(setting.SObject__c);
58 | if (objectType == null) {
59 | throw new TypeException(
60 | 'Apex Trigger Handler Setting [' +
61 | setting.Label +
62 | '] doesn\'t have a valid SObject__c [' +
63 | setting.SObject__c +
64 | '].'
65 | );
66 | } else if (preObjectType != objectType) {
67 | preObjectType = objectType;
68 | preTriggerEvent = null;
69 | handlerSettings.put(
70 | objectType,
71 | new Map>()
72 | );
73 | }
74 |
75 | TriggerOperation triggerEvent = TriggerOperation.valueOf(setting.Trigger_Event__c);
76 | if (preTriggerEvent != triggerEvent) {
77 | preTriggerEvent = triggerEvent;
78 | handlerSettings.get(objectType)
79 | .put(triggerEvent, new List());
80 | }
81 |
82 | handlerSettings.get(objectType).get(triggerEvent).add(setting);
83 | }
84 | }
85 |
86 | return handlerSettings;
87 | }
88 | set;
89 | }
90 |
91 | public List load(SObjectType sObjectType, TriggerOperation triggerEvent) {
92 | return load(sObjectType, triggerEvent, null, false);
93 | }
94 |
95 | public List load(SObjectType sObjectType, TriggerOperation triggerEvent, String tag) {
96 | return load(sObjectType, triggerEvent, tag, true);
97 | }
98 |
99 | private List load(
100 | SObjectType sObjectType,
101 | TriggerOperation triggerEvent,
102 | String tag,
103 | Boolean needCheckTag
104 | ) {
105 | List handlers = new List();
106 | if (handlerSettings.containsKey(sObjectType) && handlerSettings.get(sObjectType).containsKey(triggerEvent)) {
107 | for (Apex_Trigger_Handler_Setting__mdt setting : handlerSettings.get(sObjectType).get(triggerEvent)) {
108 | if (!needCheckTag || setting.Tag__c == tag) {
109 | Type handlerType = Type.forName(setting.Handler_Class__c);
110 | if (handlerType == null || !Triggers.Handler.class.isAssignableFrom(handlerType)) {
111 | throw new TypeException(
112 | 'Apex Trigger Handler Setting [' +
113 | setting.Label +
114 | '] doesn\'t have a valid Handler_Class__c [' +
115 | setting.Handler_Class__c +
116 | '].'
117 | );
118 | }
119 | handlers.add((Triggers.Handler) handlerType.newInstance());
120 | }
121 | }
122 | }
123 | return handlers;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/classes/TriggersLoader.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 56.0
4 | Active
5 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/classes/TriggersLoaderTest.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * BSD 3-Clause License
3 | *
4 | * Copyright (c) 2020, Jianfeng Jin
5 | * All rights reserved.
6 | *
7 | * Redistribution and use in source and binary forms, with or without
8 | * modification, are permitted provided that the following conditions are met:
9 | *
10 | * 1. Redistributions of source code must retain the above copyright notice, this
11 | * list of conditions and the following disclaimer.
12 | *
13 | * 2. Redistributions in binary form must reproduce the above copyright notice,
14 | * this list of conditions and the following disclaimer in the documentation
15 | * and/or other materials provided with the distribution.
16 | *
17 | * 3. Neither the name of the copyright holder nor the names of its
18 | * contributors may be used to endorse or promote products derived from
19 | * this software without specific prior written permission.
20 | *
21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 | */
32 |
33 | @isTest
34 | public class TriggersLoaderTest {
35 | static {
36 | TriggersLoader.settings = new List{
37 | new Apex_Trigger_Handler_Setting__mdt(
38 | SObject__c = 'Account',
39 | Trigger_Event__c = 'BEFORE_UPDATE',
40 | Handler_Class__c = 'TriggersLoaderTest.AccountTriggerHandler1',
41 | Execution_Order__c = 1,
42 | Tag__c = 'tag1'
43 | ),
44 | new Apex_Trigger_Handler_Setting__mdt(
45 | SObject__c = 'Account',
46 | Trigger_Event__c = 'BEFORE_UPDATE',
47 | Handler_Class__c = 'TriggersLoaderTest.AccountTriggerHandler2',
48 | Execution_Order__c = 2,
49 | Tag__c = 'tag2'
50 | ),
51 | new Apex_Trigger_Handler_Setting__mdt(
52 | SObject__c = 'Account',
53 | Trigger_Event__c = 'BEFORE_UPDATE',
54 | Handler_Class__c = 'TriggersLoaderTest.AccountTriggerHandler3',
55 | Execution_Order__c = 3,
56 | Tag__c = null
57 | ),
58 | new Apex_Trigger_Handler_Setting__mdt(
59 | SObject__c = 'Account',
60 | Trigger_Event__c = 'AFTER_UPDATE',
61 | Handler_Class__c = 'TriggersLoaderTest.AccountTriggerHandler3',
62 | Execution_Order__c = 1,
63 | Tag__c = 'tag1'
64 | ),
65 | new Apex_Trigger_Handler_Setting__mdt(
66 | SObject__c = 'Account',
67 | Trigger_Event__c = 'AFTER_UPDATE',
68 | Handler_Class__c = 'TriggersLoaderTest.AccountTriggerHandler2',
69 | Execution_Order__c = 2,
70 | Tag__c = 'tag1'
71 | ),
72 | new Apex_Trigger_Handler_Setting__mdt(
73 | SObject__c = 'Account',
74 | Trigger_Event__c = 'AFTER_UPDATE',
75 | Handler_Class__c = 'TriggersLoaderTest.AccountTriggerHandler1',
76 | Execution_Order__c = 3,
77 | Tag__c = 'tag2'
78 | )
79 | };
80 | }
81 |
82 | @IsTest
83 | static void testBinding_Settings_Exception_ObjectType() {
84 | TriggersLoader.settings = new List{
85 | new Apex_Trigger_Handler_Setting__mdt(
86 | SObject__c = 'XXX___InvalidAccount',
87 | Trigger_Event__c = 'BEFORE_INSERT',
88 | Handler_Class__c = 'TriggersLoaderTest.AccountTriggerHandler1',
89 | Execution_Order__c = 1,
90 | Tag__c = null
91 | )
92 | };
93 |
94 | List oldList = new List();
95 | List newList = new List();
96 |
97 | Exception exp;
98 | try {
99 | Triggers.prepare(TriggerOperation.BEFORE_INSERT, oldList, newList).beforeInsert().load().execute();
100 | } catch (Exception ex) {
101 | exp = ex;
102 | }
103 | System.debug(exp);
104 | Assert.areNotEqual(null, exp);
105 | }
106 |
107 | @IsTest
108 | static void testBinding_Settings_Exception_HandlerClass() {
109 | TriggersLoader.settings = new List{
110 | new Apex_Trigger_Handler_Setting__mdt(
111 | SObject__c = 'Account',
112 | Trigger_Event__c = 'BEFORE_INSERT',
113 | Handler_Class__c = 'TriggersLoaderTest.InvalidTriggerHandler',
114 | Execution_Order__c = 1,
115 | Tag__c = null
116 | )
117 | };
118 |
119 | List oldList = new List();
120 | List newList = new List();
121 |
122 | Exception exp;
123 | try {
124 | Triggers.prepare(TriggerOperation.BEFORE_INSERT, oldList, newList).beforeInsert().load().execute();
125 | } catch (Exception ex) {
126 | exp = ex;
127 | }
128 | System.debug(exp);
129 | Assert.areNotEqual(null, exp);
130 | }
131 |
132 | @IsTest
133 | static void testBinding_settings() {
134 | List oldList = new List{
135 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'Old Name 1'),
136 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'Old Name 2'),
137 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'Old Name 3')
138 | };
139 |
140 | List newList = new List{
141 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'New Name 1'),
142 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'New Name 2'),
143 | new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'New Name 3')
144 | };
145 |
146 | Triggers.ManagerImpl triggerManager;
147 | triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.BEFORE_UPDATE, oldList, newList);
148 | triggerManager
149 | .beforeUpdate()
150 | .bind(new MiddleTriggerHandler())
151 | .load()
152 | .bind(new MiddleTriggerHandler())
153 | .afterUpdate()
154 | .bind(new MiddleTriggerHandler())
155 | .load('tag1')
156 | .bind(new MiddleTriggerHandler())
157 | .load('tag2')
158 | .bind(new MiddleTriggerHandler())
159 | .execute();
160 | List handlerTypes = (List) triggerManager.context.state.get('handlers');
161 | Assert.areEqual(5, handlerTypes.size());
162 | Assert.areEqual(MiddleTriggerHandler.class, handlerTypes[0]);
163 | Assert.areEqual(AccountTriggerHandler1.class, handlerTypes[1]);
164 | Assert.areEqual(AccountTriggerHandler2.class, handlerTypes[2]);
165 | Assert.areEqual(AccountTriggerHandler3.class, handlerTypes[3]);
166 | Assert.areEqual(MiddleTriggerHandler.class, handlerTypes[4]);
167 |
168 | triggerManager = (Triggers.ManagerImpl) Triggers.prepare(TriggerOperation.AFTER_UPDATE, oldList, newList);
169 | triggerManager
170 | .beforeUpdate()
171 | .bind(new MiddleTriggerHandler())
172 | .load()
173 | .bind(new MiddleTriggerHandler())
174 | .afterUpdate()
175 | .bind(new MiddleTriggerHandler())
176 | .load('tag1')
177 | .bind(new MiddleTriggerHandler())
178 | .load('tag2')
179 | .bind(new MiddleTriggerHandler())
180 | .execute();
181 | handlerTypes = (List) triggerManager.context.state.get('handlers');
182 | Assert.areEqual(6, handlerTypes.size());
183 | Assert.areEqual(MiddleTriggerHandler.class, handlerTypes[0]);
184 | Assert.areEqual(AccountTriggerHandler3.class, handlerTypes[1]);
185 | Assert.areEqual(AccountTriggerHandler2.class, handlerTypes[2]);
186 | Assert.areEqual(MiddleTriggerHandler.class, handlerTypes[3]);
187 | Assert.areEqual(AccountTriggerHandler1.class, handlerTypes[4]);
188 | Assert.areEqual(MiddleTriggerHandler.class, handlerTypes[5]);
189 | }
190 |
191 | public class MiddleTriggerHandler implements Triggers.Handler, Triggers.BeforeUpdate, Triggers.AfterUpdate {
192 | public Boolean criteria(Triggers.Context context) {
193 | return Triggers.WHEN_ALWAYS;
194 | }
195 |
196 | public void beforeUpdate(Triggers.Context context) {
197 | then(context);
198 | }
199 |
200 | public void afterUpdate(Triggers.Context context) {
201 | then(context);
202 | }
203 |
204 | private void then(Triggers.Context context) {
205 | if (!context.state.containsKey('handlers')) {
206 | context.state.put('handlers', new List());
207 | }
208 | ((List) context.state.get('handlers')).add(MiddleTriggerHandler.class);
209 | }
210 | }
211 |
212 | public class AccountTriggerHandler1 implements Triggers.Handler, Triggers.BeforeUpdate, Triggers.AfterUpdate {
213 | public Boolean criteria(Triggers.Context context) {
214 | return Triggers.WHEN_ALWAYS;
215 | }
216 |
217 | public void beforeUpdate(Triggers.Context context) {
218 | then(context);
219 | }
220 |
221 | public void afterUpdate(Triggers.Context context) {
222 | then(context);
223 | }
224 |
225 | private void then(Triggers.Context context) {
226 | ((List) context.state.get('handlers')).add(AccountTriggerHandler1.class);
227 | }
228 | }
229 |
230 | public class AccountTriggerHandler2 implements Triggers.Handler, Triggers.BeforeUpdate, Triggers.AfterUpdate {
231 | public Boolean criteria(Triggers.Context context) {
232 | return Triggers.WHEN_ALWAYS;
233 | }
234 |
235 | public void beforeUpdate(Triggers.Context context) {
236 | then(context);
237 | }
238 |
239 | public void afterUpdate(Triggers.Context context) {
240 | then(context);
241 | }
242 |
243 | private void then(Triggers.Context context) {
244 | ((List) context.state.get('handlers')).add(AccountTriggerHandler2.class);
245 | }
246 | }
247 |
248 | public class AccountTriggerHandler3 implements Triggers.Handler, Triggers.BeforeUpdate, Triggers.AfterUpdate {
249 | public Boolean criteria(Triggers.Context context) {
250 | return Triggers.WHEN_ALWAYS;
251 | }
252 |
253 | public void beforeUpdate(Triggers.Context context) {
254 | then(context);
255 | }
256 |
257 | public void afterUpdate(Triggers.Context context) {
258 | then(context);
259 | }
260 |
261 | private void then(Triggers.Context context) {
262 | ((List) context.state.get('handlers')).add(AccountTriggerHandler3.class);
263 | }
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/classes/TriggersLoaderTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 56.0
4 | Active
5 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/layouts/Apex_Trigger_Handler_Setting__mdt-Apex Trigger Handler Setting Layout.layout-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 | true
6 | true
7 | Information
8 |
9 |
10 | Required
11 | MasterLabel
12 |
13 |
14 | Required
15 | DeveloperName
16 |
17 |
18 |
19 |
20 | Edit
21 | IsProtected
22 |
23 |
24 | Required
25 | NamespacePrefix
26 |
27 |
28 |
29 |
30 |
31 | true
32 | true
33 | true
34 | Handler Setting
35 |
36 |
37 | Required
38 | SObject__c
39 |
40 |
41 | Required
42 | Trigger_Event__c
43 |
44 |
45 | Required
46 | Handler_Class__c
47 |
48 |
49 |
50 |
51 | Required
52 | Execution_Order__c
53 |
54 |
55 | Edit
56 | Tag__c
57 |
58 |
59 | Edit
60 | Active__c
61 |
62 |
63 |
64 |
65 |
66 | false
67 | true
68 | false
69 | System Information
70 |
71 |
72 | Readonly
73 | CreatedById
74 |
75 |
76 |
77 |
78 | Readonly
79 | LastModifiedById
80 |
81 |
82 |
83 |
84 |
85 | true
86 | false
87 | false
88 | Custom Links
89 |
90 |
91 |
92 |
93 |
94 | false
95 | false
96 | false
97 | false
98 | false
99 |
100 | 00h9D000001kDS9
101 | 4
102 | 0
103 | Default
104 |
105 |
106 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/objects/Apex_Trigger_Handler_Setting__mdt/Apex_Trigger_Handler_Setting__mdt.object-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Apex Trigger Handler Setting
4 | Apex Trigger Handler Settings
5 | Protected
6 |
7 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/objects/Apex_Trigger_Handler_Setting__mdt/fields/Active__c.field-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Active__c
4 | true
5 | false
6 | DeveloperControlled
7 | Active
8 | Checkbox
9 |
10 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/objects/Apex_Trigger_Handler_Setting__mdt/fields/Execution_Order__c.field-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Execution_Order__c
4 | false
5 | DeveloperControlled
6 | Execution Order
7 | 5
8 | true
9 | 0
10 | Number
11 | false
12 |
13 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/objects/Apex_Trigger_Handler_Setting__mdt/fields/Handler_Class__c.field-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Handler_Class__c
4 | false
5 | DeveloperControlled
6 | Handler Class
7 | 255
8 | true
9 | Text
10 | false
11 |
12 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/objects/Apex_Trigger_Handler_Setting__mdt/fields/SObject__c.field-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SObject__c
4 | false
5 | DeveloperControlled
6 | SObject
7 | 255
8 | true
9 | Text
10 | false
11 |
12 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/objects/Apex_Trigger_Handler_Setting__mdt/fields/Tag__c.field-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Tag__c
4 | false
5 | DeveloperControlled
6 | Tag
7 | 30
8 | false
9 | Text
10 | false
11 |
12 |
--------------------------------------------------------------------------------
/apex-trigger-loader/main/default/objects/Apex_Trigger_Handler_Setting__mdt/fields/Trigger_Event__c.field-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Trigger_Event__c
4 | false
5 | DeveloperControlled
6 | Trigger Event
7 | true
8 | Picklist
9 |
10 | true
11 |
12 | false
13 |
14 | BEFORE_INSERT
15 | true
16 | BEFORE_INSERT
17 |
18 |
19 | AFTER_INSERT
20 | false
21 | AFTER_INSERT
22 |
23 |
24 | BEFORE_UPDATE
25 | false
26 | BEFORE_UPDATE
27 |
28 |
29 | AFTER_UPDATE
30 | false
31 | AFTER_UPDATE
32 |
33 |
34 | BEFORE_DELETE
35 | false
36 | BEFORE_DELETE
37 |
38 |
39 | AFTER_DELETE
40 | false
41 | AFTER_DELETE
42 |
43 |
44 | AFTER_UNDELETE
45 | false
46 | AFTER_UNDELETE
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/config/project-scratch-def.json:
--------------------------------------------------------------------------------
1 | {
2 | "orgName": "Apex Trigger Handler",
3 | "edition": "Developer",
4 | "language": "en_US",
5 | "features": ["EnableSetPasswordInApi"],
6 | "settings": {
7 | "lightningExperienceSettings": {
8 | "enableS1DesktopEnabled": true
9 | },
10 | "mobileSettings": {
11 | "enableS1EncryptedStoragePref2": false
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/docs/images/deploy-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apexfarm/ApexTriggerHandler/0ca9278f126fe688f3fe99b3a288058845787a22/docs/images/deploy-button.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "salesforce-app",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "Salesforce App",
6 | "scripts": {
7 | "lint": "eslint **/{aura,lwc}/**",
8 | "test": "npm run test:unit",
9 | "test:unit": "sfdx-lwc-jest",
10 | "test:unit:watch": "sfdx-lwc-jest --watch",
11 | "test:unit:debug": "sfdx-lwc-jest --debug",
12 | "test:unit:coverage": "sfdx-lwc-jest --coverage",
13 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"",
14 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"",
15 | "postinstall": "husky install",
16 | "precommit": "lint-staged"
17 | },
18 | "devDependencies": {
19 | "@lwc/eslint-plugin-lwc": "^1.1.2",
20 | "@prettier/plugin-xml": "^2.0.1",
21 | "@salesforce/eslint-config-lwc": "^3.2.3",
22 | "@salesforce/eslint-plugin-aura": "^2.0.0",
23 | "@salesforce/eslint-plugin-lightning": "^1.0.0",
24 | "@salesforce/sfdx-lwc-jest": "^1.1.0",
25 | "eslint": "^8.11.0",
26 | "eslint-plugin-import": "^2.25.4",
27 | "eslint-plugin-jest": "^26.1.2",
28 | "husky": "^7.0.4",
29 | "lint-staged": "^12.3.7",
30 | "prettier": "^2.6.0",
31 | "prettier-plugin-apex": "^1.10.0"
32 | },
33 | "lint-staged": {
34 | "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [
35 | "prettier --write"
36 | ],
37 | "**/{aura,lwc}/**": [
38 | "eslint"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/script.sh:
--------------------------------------------------------------------------------
1 | # force:package:create only execute for the first time
2 | # sfdx force:package:create -n ApexTriggerHandler -t Unlocked -r apex-trigger-handler
3 | sfdx force:package:version:create -p ApexTriggerHandler -x -c --wait 10 --codecoverage
4 | sfdx force:package:version:list
5 | sfdx force:package:version:promote -p 04t2v000007CfgQAAS
6 | sfdx force:package:version:report -p 04t2v000007CfgQAAS
7 |
8 |
9 | # sfdx force:package:create -n ApexTriggerHandlerExt -t Unlocked -r apex-trigger-loader
10 | sfdx force:package:version:create -p ApexTriggerHandlerExt -x -c --wait 10 --codecoverage
11 | sfdx force:package:version:list
12 | sfdx force:package:version:promote -p 04t2v000007CfgVAAS
13 | sfdx force:package:version:report -p 04t2v000007CfgVAAS
--------------------------------------------------------------------------------
/sfdx-project.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageDirectories": [
3 | {
4 | "path": "apex-trigger-handler",
5 | "package": "ApexTriggerHandler",
6 | "versionName": "ver 1.2.1",
7 | "versionNumber": "1.2.1.NEXT"
8 | },
9 | {
10 | "path": "apex-trigger-loader",
11 | "package": "ApexTriggerHandlerExt",
12 | "versionName": "ver 1.2.1",
13 | "versionNumber": "1.2.1.NEXT",
14 | "default": true,
15 | "dependencies": [
16 | {
17 | "package": "ApexTriggerHandler@1.2.1-1"
18 | }
19 | ]
20 | }
21 | ],
22 | "name": "ApexTriggerHandler",
23 | "namespace": "",
24 | "sfdcLoginUrl": "https://login.salesforce.com",
25 | "sourceApiVersion": "57.0",
26 | "packageAliases": {
27 | "ApexTriggerHandler": "0Ho2v000000PBFvCAO",
28 | "ApexTriggerHandler@1.2.0-1": "04t2v000007Cfg6AAC",
29 | "ApexTriggerHandlerExt": "0Ho2v000000PBG0CAO",
30 | "ApexTriggerHandlerExt@1.2.0-1": "04t2v000007CfgBAAS",
31 | "ApexTriggerHandler@1.2.1-1": "04t2v000007CfgQAAS",
32 | "ApexTriggerHandlerExt@1.2.1-1": "04t2v000007CfgVAAS"
33 | }
34 | }
--------------------------------------------------------------------------------