├── .gitignore ├── README.md ├── force-app └── main │ └── default │ ├── aura │ └── .eslintrc.json │ ├── classes │ ├── AccountTriggerHandler.cls │ ├── AccountTriggerHandler.cls-meta.xml │ ├── DataLayerHandler.cls │ ├── DataLayerHandler.cls-meta.xml │ ├── DocumentHelper.cls │ ├── DocumentHelper.cls-meta.xml │ ├── DocumentHelperTest.cls │ ├── DocumentHelperTest.cls-meta.xml │ ├── DocumentHelperWithDataLayer.cls │ ├── DocumentHelperWithDataLayer.cls-meta.xml │ ├── DocumentHelperWithDataLayerTest.cls │ ├── DocumentHelperWithDataLayerTest.cls-meta.xml │ ├── TestDataFactory.cls │ ├── TestDataFactory.cls-meta.xml │ ├── TestUtils.cls │ └── TestUtils.cls-meta.xml │ └── triggers │ ├── AccountTrigger.trigger │ └── AccountTrigger.trigger-meta.xml └── manifest └── package.xml /.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 | 9 | # LWC VSCode autocomplete 10 | **/lwc/jsconfig.json 11 | 12 | # LWC Jest coverage reports 13 | coverage/ 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # Eslint cache 26 | .eslintcache 27 | 28 | # MacOS system files 29 | .DS_Store 30 | 31 | # Windows system files 32 | Thumbs.db 33 | ehthumbs.db 34 | [Dd]esktop.ini 35 | $RECYCLE.BIN/ 36 | 37 | # Local environment variables 38 | .env 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mock Data Layer Pattern 2 | 3 | ###### _Presented by [Matheus Gonçalves](https://matheus.dev)_ 4 | 5 | [![Mock Data Layer](https://matheus.dev/wp-content/uploads/2021/11/MockDataLayerPattern_cover.png)](https://www.youtube.com/watch?v=sT8DIOx0x2w) 6 | 7 | Originally presented at [Salesforce Developer Group, Tampa, United States](https://trailblazercommunitygroups.com/events/details/salesforce-salesforce-developer-group-tampa-united-states-presents-speeding-up-your-apex-tests-with-a-mock-data-layer-pattern/). 8 | 9 | --- 10 | 11 | Files used in this exploratory presentation: 12 | 13 | ``` 14 | force-app 15 | main 16 | default 17 | classes 18 | ◦ AccountTriggerHandler.cls 19 | ◦ DataLayerHandler.cls 20 | ◦ DocumentHelper.cls 21 | ◦ DocumentHelperTest.cls 22 | ◦ DocumentHelperWithDataLayer 23 | ◦ DocumentHelperWithDataLayerTest 24 | ◦ TestDataFactory.cls 25 | ◦ TestUtils.cls 26 | triggers 27 | ◦ AccountTrigger.trigger 28 | ``` 29 | 30 | --- 31 | 32 | **A**pex tests are essential to the overall health of your Salesforce org. The Apex testing framework enables you to write and execute tests for your Apex classes and triggers on the Lightning Platform. Apex unit tests ensure high quality for your Apex code and let you meet the requirements for deploying Apex. 33 | 34 | It's very common to have a Test Factory for your Apex tests, creating several records, which is absolutely needed especially when testing bulk processing operations. 35 | 36 | However, besides bulk testing, you may want to test a singular method from a Helper class or run your tests with fake data. Here, instead of inserting real records in your Salesforce org, you can use the Data Layer pattern, and implement an interface that will allow you to run tests with mock data, which makes your tests run a lot faster. 37 | 38 | You can mock virtually any relationship if you use a Mock Data Layer Pattern, by adding an `Interface` to your Helper class. Let’s call it `IDataLayer`. 39 | 40 | Add this new Interface to the Helper class (here, called `DocumentHelperDataLayer.cls`). 41 | 42 | More details at [matheus.dev](https://matheus.dev/unit-test-mock-relationships-apex/). 43 | 44 | --- 45 | -------------------------------------------------------------------------------- /force-app/main/default/aura/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@salesforce/eslint-plugin-aura"], 3 | "extends": ["plugin:@salesforce/eslint-plugin-aura/recommended"], 4 | "rules": { 5 | "vars-on-top": "off", 6 | "no-unused-expressions": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AccountTriggerHandler.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : Account Trigger Handler 3 | * @author : contact@matheus.dev 4 | * @group : 5 | * @last modified on : 11-04-2021 6 | * @last modified by : contact@matheus.dev 7 | **/ 8 | public without sharing class AccountTriggerHandler { 9 | public AccountTriggerHandler() { 10 | } 11 | 12 | // ------------------------------------------------------- 13 | // AFTER INSERT 14 | // ------------------------------------------------------- 15 | public void handleAfterInsert( 16 | List newList, 17 | Map newMap 18 | ) { 19 | // Invokes the Document Helper 20 | // And gets a list of documents for insert 21 | DocumentHelper docHelper = new DocumentHelper(); 22 | 23 | List documentsForInsert = docHelper.getDocumentsForInsert( 24 | newMap 25 | ); 26 | 27 | insert documentsForInsert; 28 | } 29 | 30 | // ------------------------------------------------------- 31 | // AFTER UPDATE 32 | // ------------------------------------------------------- 33 | 34 | public void handleAfterUpdate( 35 | List oldList, 36 | Map oldMap, 37 | List newList, 38 | Map newMap 39 | ) { 40 | // Invokes our Document Helper 41 | // And gets a list of documents to update 42 | DocumentHelperWithDataLayer docHelper = new DocumentHelperWithDataLayer(); 43 | List documentsForUpdate = docHelper.getDocumentsForUpdate( 44 | newMap 45 | ); 46 | update documentsForUpdate; 47 | } 48 | 49 | // ------------------------------------------------------- 50 | // ------------------------------------------------------- 51 | // ------------------------------------------------------- 52 | 53 | public void handleBeforeInsert(List newList) { 54 | // no logic yet 55 | } 56 | 57 | public void handleBeforeUpdate( 58 | List oldList, 59 | Map oldMap, 60 | List newList, 61 | Map newMap 62 | ) { 63 | // TODO 64 | } 65 | 66 | public void handleBeforeDelete( 67 | List oldList, 68 | Map oldMap 69 | ) { 70 | // TODO 71 | } 72 | 73 | public void handleAfterDelete( 74 | List oldList, 75 | Map oldMap 76 | ) { 77 | // TODO 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AccountTriggerHandler.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DataLayerHandler.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : Data Layer class to be used by Trigger Handlers 3 | * @author : Matheus Goncalves 4 | * @last modified on : 11-01-2021 5 | * @last modified by : contact@matheus.dev 6 | **/ 7 | public class DataLayerHandler { 8 | public interface IDataLayer { 9 | List getDocumentList(); 10 | } 11 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/DataLayerHandler.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DocumentHelper.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : Helper Class for operations containing Document__c 3 | * @author : contact@matheus.dev 4 | * @group : 5 | * @last modified on : 11-03-2021 6 | * @last modified by : contact@matheus.dev 7 | **/ 8 | public with sharing class DocumentHelper { 9 | // Create the list of Documents for new Accounts 10 | public List getDocumentsForInsert(Map newMap) { 11 | List documentsForInsert = new List{}; 12 | for (Account acct : newMap.values()) { 13 | Document__c doc = new Document__c( 14 | Account__c = acct.Id, 15 | Name = acct.Name 16 | ); 17 | documentsForInsert.add(doc); 18 | } 19 | 20 | return documentsForInsert; 21 | } 22 | 23 | // ================================================== 24 | // New Requirement, recently requested 25 | // ================================================== 26 | 27 | // Make changes to the list of documents Documents 28 | public List getDocumentsForUpdate(Map newMap) { 29 | List documentsToProcess = getDocumentsLinkedToAccounts( 30 | newMap 31 | ); 32 | for (Document__c doc : documentsToProcess) { 33 | doc.Name = buildDocName(doc.Account__r.Name, doc.Contract__r.Name); 34 | } 35 | 36 | return documentsToProcess; 37 | } 38 | 39 | private String buildDocName(String accountName, String contractName) { 40 | String result = 'Document: ' + accountName + ' | ' + contractName; 41 | return result; 42 | } 43 | 44 | // Get a list of Documents linked to the Accounts 45 | // Existing Documents might be linked to Contracts 46 | private List getDocumentsLinkedToAccounts( 47 | Map newMap 48 | ) { 49 | return [ 50 | SELECT Id, Account__r.Name, Contract__r.Name 51 | FROM Document__c 52 | WHERE Account__c IN :newMap.keySet() 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DocumentHelper.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DocumentHelperTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : Simplified Test Class for DocumentHelper 3 | * @author : contact@matheus.dev 4 | * @group : 5 | * @last modified on : 11-03-2021 6 | * @last modified by : contact@matheus.dev 7 | **/ 8 | @isTest 9 | public class DocumentHelperTest { 10 | @testSetup 11 | static void setup() { 12 | // Create common test accounts 13 | List testAccounts = TestDataFactory.createAccountsForInsert( 14 | 201 15 | ); 16 | insert testAccounts; 17 | } 18 | 19 | @IsTest 20 | static void test_get_documents_for_insert() { 21 | Map newMap = new Map( 22 | [SELECT Id, Name FROM Account] 23 | ); 24 | 25 | System.assert(!newMap?.isEmpty(), 'Map of accounts is empty'); 26 | 27 | Test.startTest(); 28 | 29 | // Query documents 30 | List newDocs = [ 31 | SELECT Id, Name, Account__c 32 | FROM Document__c 33 | WHERE Account__c IN :newMap.keySet() 34 | ]; 35 | Test.stopTest(); 36 | 37 | System.assertEquals( 38 | newMap.size(), 39 | newDocs.size(), 40 | 'The amount of new docs is incorrect.' 41 | ); 42 | } 43 | 44 | // Test for the new requirement 45 | @IsTest 46 | static void test_get_documents_for_update() { 47 | Map newMap = new Map( 48 | [SELECT Id, Name FROM Account] 49 | ); 50 | 51 | System.assert(!newMap?.isEmpty(), 'Map of accounts is empty'); 52 | 53 | // Query documents 54 | List newDocs = [ 55 | SELECT Id, Name, Account__c 56 | FROM Document__c 57 | WHERE Account__c IN :newMap.keySet() 58 | ]; 59 | 60 | System.assertEquals( 61 | newMap.size(), 62 | newDocs.size(), 63 | 'The amount of new docs is incorrect.' 64 | ); 65 | 66 | Test.startTest(); 67 | 68 | String uniqueValidator = 'demo-data-layer-tampa-fl'; 69 | // Create Contracts 70 | List contractsForInsert = TestDataFactory.createContractsForInsert( 71 | newDocs, 72 | uniqueValidator 73 | ); 74 | 75 | insert contractsForInsert; 76 | 77 | List accountsForUpdate = new List{}; 78 | 79 | // Update the Accounts to trigger the update on Documents 80 | for (Account acctForUpdate : newMap.values()) { 81 | acctForUpdate.Name = acctForUpdate.Name + ' | ' + uniqueValidator; 82 | accountsForUpdate.add(acctForUpdate); 83 | } 84 | 85 | update accountsForUpdate; 86 | 87 | // Re-query documents to check if the new method worked 88 | newDocs = [ 89 | SELECT Id, Name, Account__c 90 | FROM Document__c 91 | WHERE Account__c IN :newMap.keySet() 92 | ]; 93 | 94 | for (Document__c docForAssertion : newDocs) { 95 | system.assert( 96 | docForAssertion.Name?.containsIgnoreCase(uniqueValidator), 97 | 'The unique validator could not be found' 98 | ); 99 | } 100 | 101 | Test.stopTest(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DocumentHelperTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DocumentHelperWithDataLayer.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : Helper Class for operations containing Document__c with Data Layer 3 | * @author : contact@matheus.dev 4 | * @group : 5 | * @last modified on : 11-04-2021 6 | * @last modified by : contact@matheus.dev 7 | **/ 8 | public with sharing class DocumentHelperWithDataLayer { 9 | // Create a private dataLayer here 10 | private IDataLayer dataLayer; 11 | 12 | // For an empty constructor, you create a new instance of the DataLayer: 13 | // TODO 14 | public DocumentHelperWithDataLayer() { 15 | this(new DataLayer()); 16 | } 17 | 18 | // If the constructor contains a dataLayer as a parameter, use it in your class. 19 | // TODO 20 | public DocumentHelperWithDataLayer(IDataLayer dataLayer) { 21 | this.dataLayer = dataLayer; 22 | } 23 | 24 | // Existing Methods 25 | public List getDocumentsForUpdate(Map newMap) { 26 | List documentsToProcess = dataLayer.getDocumentsLinkedToAccounts( 27 | newMap 28 | ); 29 | for (Document__c doc : documentsToProcess) { 30 | doc.Name = buildDocName(doc.Account__r.Name, doc.Contract__r.Name); 31 | } 32 | 33 | return documentsToProcess; 34 | } 35 | 36 | private String buildDocName(String accountName, String contractName) { 37 | String result = 'Document: ' + accountName + ' | ' + contractName; 38 | return result; 39 | } 40 | 41 | // Create DataLayer class here 42 | public class DataLayer implements IDataLayer { 43 | public List getDocumentsLinkedToAccounts( 44 | Map newMap 45 | ) { 46 | return [ 47 | SELECT Id, Account__r.Name, Contract__r.Name 48 | FROM Document__c 49 | WHERE Account__c IN :newMap.keySet() 50 | ]; 51 | } 52 | } 53 | 54 | // Create Interface here: 55 | public interface IDataLayer { 56 | List getDocumentsLinkedToAccounts(Map newMap); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DocumentHelperWithDataLayer.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DocumentHelperWithDataLayerTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : Simplified Test Class for DocumentHelperWithDataLayer 3 | * @author : contact@matheus.dev 4 | * @group : 5 | * @last modified on : 11-04-2021 6 | * @last modified by : contact@matheus.dev 7 | **/ 8 | @isTest 9 | public class DocumentHelperWithDataLayerTest { 10 | // Create a MockDataLayer here: 11 | private static MockDataLayer dataLayer; 12 | private static DocumentHelperWithDataLayer testedClass; 13 | 14 | static { 15 | dataLayer = new MockDataLayer(); 16 | } 17 | 18 | // Test for the new requirement 19 | @IsTest 20 | static void test_get_documents_for_update() { 21 | // Now, let's mock some data: 22 | // consider using a Mock Data Factory, but for this example, let's create a test Account: 23 | Account testAccount = new Account( 24 | Name = 'TestAccount', 25 | Id = TestUtils.getFakeId(Schema.Account.SObjectType) 26 | ); 27 | 28 | dataLayer.documentAccounts.add(testAccount); 29 | 30 | Contract testContract = new Contract( 31 | Name = 'TestContract', 32 | Id = TestUtils.getFakeId(Schema.Contract.SObjectType) 33 | ); 34 | 35 | dataLayer.documentContracts.add(testContract); 36 | 37 | Document__c testDocument = new Document__c( 38 | Name = 'TestDocument', 39 | Id = TestUtils.getFakeId(Schema.Document__c.SObjectType), 40 | Account__r = testAccount, 41 | Contract__r = testContract 42 | ); 43 | 44 | Document__c testDocumentNoContract = new Document__c( 45 | Name = 'TestDocument', 46 | Id = TestUtils.getFakeId(Schema.Document__c.SObjectType), 47 | Account__r = testAccount 48 | ); 49 | 50 | dataLayer.mockDocuments.add(testDocumentNoContract); 51 | 52 | // Now, all you need to do is to create a new instance of you handler class, 53 | // but passing your mock data layer to the constructor! 54 | 55 | // Since the Interface structure is the same, 56 | // the mock version is accepted by the class constructor. 57 | testedClass = new DocumentHelperWithDataLayer(dataLayer); 58 | 59 | Test.startTest(); 60 | Map newMap = new Map( 61 | dataLayer.documentAccounts 62 | ); 63 | 64 | List testDocuments = testedClass.getDocumentsForUpdate( 65 | newMap 66 | ); 67 | Test.stopTest(); 68 | 69 | // Run the assertions 70 | for (Document__c docForUpdate : testDocuments) { 71 | system.assert( 72 | docForUpdate.Name?.containsIgnoreCase(testAccount.Name), 73 | 'The document does not contain the account name' 74 | ); 75 | if (docForUpdate.Contract__r != null) { 76 | system.assert( 77 | docForUpdate.Name?.containsIgnoreCase(testContract.Name), 78 | 'The document does not contain the contract name' 79 | ); 80 | } 81 | } 82 | } 83 | 84 | // Create the MockDataLayer class here 85 | public class MockDataLayer implements DocumentHelperWithDataLayer.IDataLayer { 86 | List documentAccounts = new List(); 87 | List documentContracts = new List(); 88 | List mockDocuments = new List(); 89 | 90 | public List getDocumentsLinkedToAccounts( 91 | Map newMap 92 | ) { 93 | // Here we can just return the mock data. 94 | return mockDocuments; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DocumentHelperWithDataLayerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/TestDataFactory.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : Simplified Test Factory 3 | * @author : contact@matheus.dev 4 | * @group : 5 | * @last modified on : 11-03-2021 6 | * @last modified by : contact@matheus.dev 7 | **/ 8 | public class TestDataFactory { 9 | public static Account createAccountForInsert(String accountName) { 10 | return new Account(Name = accountName); 11 | } 12 | 13 | public static List createAccountsForInsert(Integer recordCount) { 14 | List resultAccounts = new List{}; 15 | for (Integer i = 0; i <= recordCount; i++) { 16 | resultAccounts.add(createAccountForInsert('Test Account ' + i)); 17 | } 18 | 19 | return resultAccounts; 20 | } 21 | 22 | public static Contract createContractForInsert( 23 | String uniqueValidator, 24 | Document__c document 25 | ) { 26 | return new Contract( 27 | Name = uniqueValidator + ' | Test Contract | ' + document.Name, 28 | AccountId = document.Account__c 29 | ); 30 | } 31 | 32 | public static List createContractsForInsert( 33 | List documents, 34 | String uniqueValidator 35 | ) { 36 | List resultContracts = new List{}; 37 | for (Document__c document : documents) { 38 | resultContracts.add( 39 | createContractForInsert(uniqueValidator, document) 40 | ); 41 | } 42 | 43 | return resultContracts; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /force-app/main/default/classes/TestDataFactory.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/TestUtils.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : Test Utils 3 | * @author : contact@matheus.dev 4 | * @group : 5 | * @last modified on : 11-03-2021 6 | * @last modified by : contact@matheus.dev 7 | **/ 8 | public class TestUtils { 9 | private static Integer fakeIdCounter = 1; 10 | 11 | public static String getFakeId(Schema.SObjectType type) { 12 | String result = String.valueOf(fakeIdCounter++); 13 | return type.getDescribe().getKeyPrefix() + 14 | '0'.repeat(12 - result.length()) + 15 | result; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /force-app/main/default/classes/TestUtils.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/triggers/AccountTrigger.trigger: -------------------------------------------------------------------------------- 1 | /** 2 | * @description : 3 | * @author : contact@matheus.dev 4 | * @group : 5 | * @last modified on : 11-02-2021 6 | * @last modified by : contact@matheus.dev 7 | **/ 8 | trigger AccountTrigger on Account( 9 | before insert, 10 | before update, 11 | before delete, 12 | after insert, 13 | after update, 14 | after delete 15 | ) { 16 | AccountTriggerHandler handler = new AccountTriggerHandler(); 17 | 18 | if (Trigger.isBefore && Trigger.isInsert) { 19 | handler.handleBeforeInsert(Trigger.new); 20 | } else if (Trigger.isBefore && Trigger.isUpdate) { 21 | handler.handleBeforeUpdate( 22 | Trigger.old, 23 | Trigger.oldMap, 24 | Trigger.new, 25 | Trigger.newMap 26 | ); 27 | } else if (Trigger.isBefore && Trigger.isDelete) { 28 | handler.handleBeforeDelete(Trigger.old, Trigger.oldMap); 29 | } else if (Trigger.isAfter && Trigger.isInsert) { 30 | handler.handleAfterInsert(Trigger.new, Trigger.newMap); 31 | } else if (Trigger.isAfter && Trigger.isUpdate) { 32 | handler.handleAfterUpdate( 33 | Trigger.old, 34 | Trigger.oldMap, 35 | Trigger.new, 36 | Trigger.newMap 37 | ); 38 | } else if (Trigger.isAfter && Trigger.isDelete) { 39 | handler.handleAfterDelete(Trigger.old, Trigger.oldMap); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /force-app/main/default/triggers/AccountTrigger.trigger-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52.0 4 | Active 5 | -------------------------------------------------------------------------------- /manifest/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | * 5 | ApexClass 6 | 7 | 8 | * 9 | ApexComponent 10 | 11 | 12 | * 13 | ApexPage 14 | 15 | 16 | * 17 | ApexTestSuite 18 | 19 | 20 | * 21 | ApexTrigger 22 | 23 | 24 | * 25 | AuraDefinitionBundle 26 | 27 | 28 | * 29 | LightningComponentBundle 30 | 31 | 32 | * 33 | StaticResource 34 | 35 | 52.0 36 | --------------------------------------------------------------------------------