├── Add a Mobile Card that Displays Content Based on Opportunity Stage └── OpportunityStageTips.vfp ├── Add a Visualforce Page for a Contact Record Action └── ShowAssistantInfo.vfp ├── Create a Form to Enter New Items ├── README.md ├── campingHeader.cmp ├── campingList.cmp ├── campingListController.js └── campingListItem.cmp ├── Create a Global Action that Displays a User's Business Card Information └── BusinessCard.vfp ├── Create a Selector class for the Account object ├── AccountsSelector.apxc └── fflib_SObjectSelector.cls ├── Create a Visualforce Page ├── EmailMissionSpecialist.apxc └── StationCheck.vfp ├── Create a Visualforce form which inserts a basic Contact record └── CreateContact.vfp ├── Create a Visualforce page displaying new cases ├── NewCaseList.vfp └── NewCaseListController.apxc ├── Create a Visualforce page that shows user information └── DisplayUserInfo.vfp ├── Create a Visualforce page which displays a variety of output fields └── OppView.vfp ├── Create a Visualforce page which shows a basic Contact record └── ContactView.vpf ├── Create a Visualforce page which shows a list of Accounts linked to their record pages └── AccountList.vfp ├── Create a mobile-friendly Visualforce page using SLDS └── MobileContactList.vpf ├── Create a simple Visualforce page that displays an image └── displayImage.vfp ├── Create an Apex class that returns Account objects └── AccountUtils.apxc ├── Create an Apex class that returns both contacts and leads based on a parameter ├── ContactAndLeadSearch.apex └── debug_console.apex ├── Create and deploy a custom big object ├── Rider_History__b.object ├── package.xml └── rider_history.permissionset ├── Implement a basic Domain class and Apex trigger ├── Accounts.apxc ├── AccountsTrigger.apxt └── fflib_SObjectDomain.cls ├── Refactor Components and Communicate with Events ├── README.md ├── addItemEvent.evt ├── campingList.cmp ├── campingListController.js ├── campingListForm.cpm ├── campingListFormController.js ├── campingListFormHelper.js └── campingListHelper.js ├── Save and Load Records with a Server-Side Controller ├── CampingListController.cls ├── CampingListController.js ├── README.md ├── campingList.cmp └── campingListHelper.js ├── Subscribe to a Platform Event in an Apex Trigger └── OrderEventTrigger.apex ├── Use Apex Metadata API to add a custom field to a page layout ├── UpdateContactPageLayout.apxc └── Use Apex Metadata API to add a custom metadata type to an org │ └── MetadataExample.apxc ├── Use a static resource to display an image on a Visualforce Page ├── ShowImage.vfp └── vfimagetest.zip └── Write an Apex trigger that modifies Account fields before inserting records ├── AccountTrigger.apxt ├── AccountTriggerHandler.apxc └── AccountTriggerTest.apxc /Add a Mobile Card that Displays Content Based on Opportunity Stage/OpportunityStageTips.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Prospecting 5 | 6 | 7 | 8 | Needs Analysis 9 | 10 | 11 | 12 | Proposal/Price Quite 13 | 14 | 15 | 16 | Negotiation/Review 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Add a Visualforce Page for a Contact Record Action/ShowAssistantInfo.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Assistant:{!AssistantName} 6 |

7 |

8 | Phone:{!AssistantPhone} 9 |

10 |
11 | -------------------------------------------------------------------------------- /Create a Form to Enter New Items/README.md: -------------------------------------------------------------------------------- 1 | # Challenge Description 2 | **Create a Form to Enter New Items** 3 | 4 | In this challenge you'll create a form to enter new items, a list to display the items entered, and add SLDS styling. First, to make our camping list look more appealing, change the campingHeader component to use lightning:layout and SLDS. Similar to the unit, style the Camping List H1 inside the slds-page-header. Add the action:goal SLDS icon using lightning:icon. 5 | 6 | Next, modify the campingList component to contain a new item input form and an iteration of campingListItem components for displaying the items entered. Here are additional details for the modifications to the campingList component. 7 | 8 | In this challenge you'll create a form to enter new items, a list to display the items entered, and add SLDS styling. First, to make our camping list look more appealing, change the campingHeader component to use lightning:layout and SLDS. Similar to the unit, style the Camping List H1 inside the slds-page-header. Add the action:goal SLDS icon using lightning:icon. 9 | 10 | Next, modify the campingList component to contain a new item input form and an iteration of campingListItem components for displaying the items entered. Here are additional details for the modifications to the campingList component. 11 | 12 | * Add an attribute named items with the type of an array of camping item custom objects. 13 | * Add an attribute named newItem of type Camping_Item__c with default quantity and price values of 0. 14 | * The component displays the Name, Quantity, Price, and Packed form fields with the appropriate input component types and values from the newItem attribute. The Quantity field accepts a number that's at least 1. 15 | * Submitting the form executes the action clickCreateItem in the JavaScript controller. 16 | * If the form is valid, the JavaScript controller pushes the newItem onto the array of existing items, triggers the notification that the items value provider has changed, and resets the newItem value provider with a blank sObjectType of Camping_Item__c. For this challenge, place the code in your component's controller, not the helper. 17 | 18 | -------------------------------------------------------------------------------- /Create a Form to Enter New Items/campingHeader.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Create a Form to Enter New Items/campingList.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 | 20 | 22 | Add Expense 23 | 24 | 25 | 26 |
27 | 28 | 29 | 33 | 34 | 39 | 40 | 45 | 46 | 49 | 50 | 54 | 55 | 56 | 57 |
58 | 59 | 60 |
61 | 62 |
63 | 64 | 65 |
66 | 67 | 68 | 69 |
70 |
71 | 72 | 73 | 74 |
75 |
76 |
77 | 78 |
79 | -------------------------------------------------------------------------------- /Create a Form to Enter New Items/campingListController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | clickCreateItem : function(component, event, helper) { 3 | var validCamping = component.find('campingform').reduce(function (validSoFar, inputCmp) { 4 | // Displays error messages for invalid fields 5 | inputCmp.showHelpMessageIfInvalid(); 6 | return validSoFar && inputCmp.get('v.validity').valid; 7 | }, true); 8 | 9 | if(validCamping){ 10 | var newCampingItem = component.get("v.newItem"); 11 | //helper.createCamping(component,newCampingItem); 12 | var campings = component.get("v.items"); 13 | var item = JSON.parse(JSON.stringify(newCampingItem)); 14 | 15 | campings.push(item); 16 | 17 | component.set("v.items",campings); 18 | component.set("v.newItem",{ 'sobjectType': 'Camping_Item__c','Name': '','Quantity__c': 0, 19 | 'Price__c': 0,'Packed__c': false }); 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /Create a Form to Enter New Items/campingListItem.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Name: 5 | 6 |

7 |

Price: 8 | 9 |

10 |

Quantity: 11 | 12 |

13 |

Packed: 14 | 15 |

16 |
17 | -------------------------------------------------------------------------------- /Create a Global Action that Displays a User's Business Card Information/BusinessCard.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Quick Share

5 |
  • {!$User.FirstName}
  • 6 |
  • {!$User.LastName}
  • 7 |
  • {!$User.Email}
  • 8 |
  • {!$User.Phone}
  • 9 |
  • {!$User.Title}
  • 10 | 11 | 12 |
    13 | 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /Create a Selector class for the Account object/AccountsSelector.apxc: -------------------------------------------------------------------------------- 1 | public class AccountsSelector extends fflib_SObjectSelector { 2 | 3 | public List getSObjectFieldList() { 4 | return new List { 5 | Account.ID, Account.Description, Account.Name, Account.AnnualRevenue 6 | }; 7 | } 8 | 9 | public Schema.SObjectType getSObjectType() { 10 | return Account.SObjectType; 11 | } 12 | 13 | public List selectById(Set idSet) { 14 | List acctList = new List (); 15 | acctList = (List) selectSObjectsById(idSet); 16 | return acctList; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Create a Selector class for the Account object/fflib_SObjectSelector.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c), FinancialForce.com, inc 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * - Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * - Redistributions in binary form must reproduce the above copyright notice, 11 | * this list of conditions and the following disclaimer in the documentation 12 | * and/or other materials provided with the distribution. 13 | * - Neither the name of the FinancialForce.com, inc nor the names of its contributors 14 | * may be used to endorse or promote products derived from this software without 15 | * specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 20 | * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 23 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | **/ 26 | 27 | /** 28 | * Class providing common database query support for abstracting and encapsulating query logic 29 | **/ 30 | public abstract with sharing class fflib_SObjectSelector 31 | implements fflib_ISObjectSelector 32 | { 33 | /** 34 | * Indicates whether the sObject has the currency ISO code field for organisations which have multi-currency 35 | * enabled. 36 | **/ 37 | private Boolean CURRENCY_ISO_CODE_ENABLED { 38 | get { 39 | if(CURRENCY_ISO_CODE_ENABLED == null){ 40 | CURRENCY_ISO_CODE_ENABLED = describeWrapper.getFieldsMap().keySet().contains('currencyisocode'); 41 | } 42 | return CURRENCY_ISO_CODE_ENABLED; 43 | } 44 | set; 45 | } 46 | 47 | /** 48 | * Should this selector automatically include the FieldSet fields when building queries? 49 | **/ 50 | private Boolean m_includeFieldSetFields; 51 | 52 | /** 53 | * Enforce FLS Security 54 | **/ 55 | private Boolean m_enforceFLS; 56 | 57 | /** 58 | * Enforce CRUD Security 59 | **/ 60 | private Boolean m_enforceCRUD; 61 | 62 | /** 63 | * Order by field 64 | **/ 65 | private String m_orderBy; 66 | 67 | /** 68 | * Sort the query fields in the select statement (defaults to true, at the expense of performance). 69 | * Switch this off if you need more performant queries. 70 | **/ 71 | private Boolean m_sortSelectFields; 72 | 73 | /** 74 | * Describe helper 75 | **/ 76 | private fflib_SObjectDescribe describeWrapper { 77 | get { 78 | if(describeWrapper == null) 79 | describeWrapper = fflib_SObjectDescribe.getDescribe(getSObjectType()); 80 | return describeWrapper; 81 | } 82 | set; 83 | } 84 | /** 85 | * static variables 86 | **/ 87 | private static String DEFAULT_SORT_FIELD = 'CreatedDate'; 88 | private static String SF_ID_FIELD = 'Id'; 89 | 90 | /** 91 | * Implement this method to inform the base class of the SObject (custom or standard) to be queried 92 | **/ 93 | abstract Schema.SObjectType getSObjectType(); 94 | 95 | /** 96 | * Implement this method to inform the base class of the common fields to be queried or listed by the base class methods 97 | **/ 98 | abstract List getSObjectFieldList(); 99 | 100 | /** 101 | * Constructs the Selector, defaults to not including any FieldSet fields automatically 102 | **/ 103 | public fflib_SObjectSelector() 104 | { 105 | this(false); 106 | } 107 | 108 | /** 109 | * Constructs the Selector 110 | * 111 | * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well 112 | **/ 113 | public fflib_SObjectSelector(Boolean includeFieldSetFields) 114 | { 115 | this(includeFieldSetFields, true, false); 116 | } 117 | 118 | /** 119 | * Constructs the Selector 120 | * 121 | * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well 122 | **/ 123 | public fflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS) 124 | { 125 | this(includeFieldSetFields, enforceCRUD, enforceFLS, true); 126 | } 127 | 128 | /** 129 | * Constructs the Selector 130 | * 131 | * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well 132 | * @param enforceCRUD Enforce CRUD security 133 | * @param enforeFLS Enforce Field Level Security 134 | * @param sortSelectFields Set to false if selecting many columns to skip sorting select fields and improve performance 135 | **/ 136 | public fflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS, Boolean sortSelectFields) 137 | { 138 | m_includeFieldSetFields = includeFieldSetFields; 139 | m_enforceCRUD = enforceCRUD; 140 | m_enforceFLS = enforceFLS; 141 | m_sortSelectFields = sortSelectFields; 142 | } 143 | 144 | /** 145 | * Override this method to provide a list of Fieldsets that can optionally drive inclusion of additional fields in the base queries 146 | **/ 147 | public virtual List getSObjectFieldSetList() 148 | { 149 | return null; 150 | } 151 | 152 | /** 153 | * Override this method to control the default ordering of records returned by the base queries, 154 | * defaults to the name field of the object if it is not encrypted or CreatedDate if there the object has createdDated or Id 155 | **/ 156 | public virtual String getOrderBy() 157 | { 158 | if (m_orderBy == null) 159 | { 160 | Schema.SObjectField nameField = describeWrapper.getNameField(); 161 | if (nameField != null && !nameField.getDescribe().isEncrypted()) 162 | { 163 | m_orderBy = nameField.getDescribe().getName(); 164 | } 165 | else 166 | { 167 | m_orderBy = DEFAULT_SORT_FIELD; 168 | try { 169 | if (describeWrapper.getField(m_orderBy) == null) 170 | { 171 | m_orderBy = SF_ID_FIELD; 172 | } 173 | } 174 | catch(fflib_QueryFactory.InvalidFieldException ex) { 175 | m_orderBy = SF_ID_FIELD; 176 | } 177 | } 178 | } 179 | return m_orderBy; 180 | } 181 | 182 | /** 183 | * Returns True if this Selector instance has been instructed by the caller to include Field Set fields 184 | **/ 185 | public Boolean isIncludeFieldSetFields() 186 | { 187 | return m_includeFieldSetFields; 188 | } 189 | 190 | /** 191 | * Returns True if this Selector is enforcing FLS 192 | **/ 193 | public Boolean isEnforcingFLS() 194 | { 195 | return m_enforceFLS; 196 | } 197 | 198 | /** 199 | * Returns True if this Selector is enforcing CRUD Security 200 | **/ 201 | public Boolean isEnforcingCRUD() 202 | { 203 | return m_enforceCRUD; 204 | } 205 | 206 | /** 207 | * Provides access to the builder containing the list of fields base queries are using, this is demand 208 | * created if one has not already been defined via setFieldListBuilder 209 | * 210 | * @depricated See newQueryFactory 211 | **/ 212 | public fflib_StringBuilder.CommaDelimitedListBuilder getFieldListBuilder() 213 | { 214 | return 215 | new fflib_StringBuilder.CommaDelimitedListBuilder( 216 | new List(newQueryFactory().getSelectedFields())); 217 | } 218 | 219 | /** 220 | * Use this method to override the default FieldListBuilder (created on demand via getFieldListBuilder) with a custom one, 221 | * warning, this will bypass anything getSObjectFieldList or getSObjectFieldSetList returns 222 | * 223 | * @depricated See newQueryFactory 224 | **/ 225 | public void setFieldListBuilder(fflib_StringBuilder.FieldListBuilder fieldListBuilder) 226 | { 227 | // TODO: Consider if given the known use cases for this (dynamic selector optomisation) if it's OK to leave this as a null operation 228 | } 229 | 230 | /** 231 | * Returns in string form a comma delimted list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList 232 | * 233 | * @depricated See newQueryFactory 234 | **/ 235 | public String getFieldListString() 236 | { 237 | return getFieldListBuilder().getStringValue(); 238 | } 239 | 240 | /** 241 | * Returns in string form a comma delimted list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList 242 | * @param relation Will prefix fields with the given relation, e.g. MyLookupField__r 243 | * 244 | * @depricated See newQueryFactory 245 | **/ 246 | public String getRelatedFieldListString(String relation) 247 | { 248 | return getFieldListBuilder().getStringValue(relation + '.'); 249 | } 250 | 251 | /** 252 | * Returns the string representaiton of the SObject this selector represents 253 | **/ 254 | public String getSObjectName() 255 | { 256 | return describeWrapper.getDescribe().getName(); 257 | } 258 | 259 | /** 260 | * Performs a SOQL query, 261 | * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included) 262 | * - From the SObject described by getSObjectType 263 | * - Where the Id's match those provided in the set 264 | * - Ordered by the fields returned via getOrderBy 265 | * @returns A list of SObject's 266 | **/ 267 | public List selectSObjectsById(Set idSet) 268 | { 269 | return Database.query(buildQuerySObjectById()); 270 | } 271 | 272 | /** 273 | * Performs a SOQL query, 274 | * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included) 275 | * - From the SObject described by getSObjectType 276 | * - Where the Id's match those provided in the set 277 | * - Ordered by the fields returned via getOrderBy 278 | * @returns A QueryLocator (typically for use in a Batch Apex job) 279 | **/ 280 | public Database.QueryLocator queryLocatorById(Set idSet) 281 | { 282 | return Database.getQueryLocator(buildQuerySObjectById()); 283 | } 284 | 285 | /** 286 | * Throws an exception if the SObject indicated by getSObjectType is not accessible to the current user (read access) 287 | * 288 | * @depricated If you utilise the newQueryFactory method this is automatically done for you (unless disabled by the selector) 289 | **/ 290 | public void assertIsAccessible() 291 | { 292 | if(!getSObjectType().getDescribe().isAccessible()) 293 | throw new fflib_SObjectDomain.DomainException( 294 | 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); 295 | } 296 | 297 | /** 298 | * Public acccess for the getSObjectType during Mock registration 299 | * (adding public to the existing method broken base class API backwards compatability) 300 | **/ 301 | public SObjectType getSObjectType2() 302 | { 303 | return getSObjectType(); 304 | } 305 | 306 | /** 307 | * Public acccess for the getSObjectType during Mock registration 308 | * (adding public to the existing method broken base class API backwards compatability) 309 | **/ 310 | public SObjectType sObjectType() 311 | { 312 | return getSObjectType(); 313 | } 314 | 315 | /** 316 | * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by 317 | **/ 318 | public fflib_QueryFactory newQueryFactory() 319 | { 320 | return newQueryFactory(m_enforceCRUD, m_enforceFLS, true); 321 | } 322 | 323 | /** 324 | * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by 325 | **/ 326 | public fflib_QueryFactory newQueryFactory(Boolean includeSelectorFields) 327 | { 328 | return newQueryFactory(m_enforceCRUD, m_enforceFLS, includeSelectorFields); 329 | } 330 | 331 | /** 332 | * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by 333 | * CRUD and FLS read security will be checked if the corresponding inputs are true (overrides that defined in the selector). 334 | **/ 335 | public fflib_QueryFactory newQueryFactory(Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields) 336 | { 337 | // Construct QueryFactory around the given SObject 338 | return configureQueryFactory( 339 | new fflib_QueryFactory(getSObjectType2()), 340 | assertCRUD, enforceFLS, includeSelectorFields); 341 | } 342 | 343 | /** 344 | * Adds the selectors fields to the given QueryFactory using the given relationship path as a prefix 345 | * 346 | * // TODO: This should be consistant (ideally) with configureQueryFactory below 347 | **/ 348 | public void configureQueryFactoryFields(fflib_QueryFactory queryFactory, String relationshipFieldPath) 349 | { 350 | // Add fields from selector prefixing the relationship path 351 | for(SObjectField field : getSObjectFieldList()) 352 | queryFactory.selectField(relationshipFieldPath + '.' + field.getDescribe().getName()); 353 | // Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule) 354 | if(Userinfo.isMultiCurrencyOrganization() && CURRENCY_ISO_CODE_ENABLED) 355 | queryFactory.selectField(relationshipFieldPath+'.CurrencyIsoCode'); 356 | } 357 | 358 | /** 359 | * Adds a subselect QueryFactory based on this selector to the given QueryFactor, returns the parentQueryFactory 360 | **/ 361 | public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory) 362 | { 363 | return addQueryFactorySubselect(parentQueryFactory, true); 364 | } 365 | 366 | /** 367 | * Adds a subselect QueryFactory based on this selector to the given QueryFactor 368 | **/ 369 | public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, Boolean includeSelectorFields) 370 | { 371 | fflib_QueryFactory subSelectQueryFactory = 372 | parentQueryFactory.subselectQuery(getSObjectType2()); 373 | return configureQueryFactory( 374 | subSelectQueryFactory, 375 | m_enforceCRUD, 376 | m_enforceFLS, 377 | includeSelectorFields); 378 | } 379 | 380 | /** 381 | * Adds a subselect QueryFactory based on this selector to the given QueryFactor, returns the parentQueryFactory 382 | **/ 383 | public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, String relationshipName) 384 | { 385 | return addQueryFactorySubselect(parentQueryFactory, relationshipName, TRUE); 386 | } 387 | 388 | /** 389 | * Adds a subselect QueryFactory based on this selector to the given QueryFactor 390 | **/ 391 | public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, String relationshipName, Boolean includeSelectorFields) 392 | { 393 | fflib_QueryFactory subSelectQueryFactory = parentQueryFactory.subselectQuery(relationshipName); 394 | return configureQueryFactory(subSelectQueryFactory, m_enforceCRUD, m_enforceFLS, includeSelectorFields); 395 | } 396 | 397 | /** 398 | * Constructs the default SOQL query for this selector, see selectSObjectsById and queryLocatorById 399 | **/ 400 | private String buildQuerySObjectById() 401 | { 402 | return newQueryFactory().setCondition('id in :idSet').toSOQL(); 403 | } 404 | 405 | /** 406 | * Configures a QueryFactory instance according to the configuration of this selector 407 | **/ 408 | private fflib_QueryFactory configureQueryFactory(fflib_QueryFactory queryFactory, Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields) 409 | { 410 | // CRUD and FLS security required? 411 | if (assertCRUD) 412 | { 413 | try { 414 | // Leverage QueryFactory for CRUD checking 415 | queryFactory.assertIsAccessible(); 416 | } catch (fflib_SecurityUtils.CrudException e) { 417 | // Marshal exception into DomainException for backwards compatability 418 | throw new fflib_SObjectDomain.DomainException( 419 | 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); 420 | } 421 | } 422 | queryFactory.setEnforceFLS(enforceFLS); 423 | 424 | // Configure the QueryFactory with the Selector fields? 425 | if(includeSelectorFields) 426 | { 427 | // select the Selector fields and Fieldsets and set order 428 | queryFactory.selectFields(getSObjectFieldList()); 429 | 430 | List fieldSetList = getSObjectFieldSetList(); 431 | if(m_includeFieldSetFields && fieldSetList != null) 432 | for(Schema.FieldSet fieldSet : fieldSetList) 433 | queryFactory.selectFieldSet(fieldSet); 434 | 435 | // Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule) 436 | if(Userinfo.isMultiCurrencyOrganization() && CURRENCY_ISO_CODE_ENABLED) 437 | queryFactory.selectField('CurrencyIsoCode'); 438 | } 439 | 440 | // Parse the getOrderBy() 441 | for(String orderBy : getOrderBy().split(',')) 442 | { 443 | List orderByParts = orderBy.trim().split(' '); 444 | String fieldNamePart = orderByParts[0]; 445 | String fieldSortOrderPart = orderByParts.size() > 1 ? orderByParts[1] : null; 446 | fflib_QueryFactory.SortOrder fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING; 447 | if(fieldSortOrderPart==null) 448 | fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING; 449 | else if(fieldSortOrderPart.equalsIgnoreCase('DESC')) 450 | fieldSortOrder = fflib_QueryFactory.SortOrder.DESCENDING; 451 | else if(fieldSortOrderPart.equalsIgnoreCase('ASC')) 452 | fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING; 453 | queryFactory.addOrdering(fieldNamePart, fieldSortOrder, orderBy.containsIgnoreCase('NULLS LAST')); 454 | } 455 | 456 | queryFactory.setSortSelectFields(m_sortSelectFields); 457 | 458 | return queryFactory; 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /Create a Visualforce Page/EmailMissionSpecialist.apxc: -------------------------------------------------------------------------------- 1 | public class EmailMissionSpecialist { 2 | // Public method 3 | public void sendMail(String address, String subject, String body) { 4 | // Create an email message object 5 | Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); 6 | String[] toAddresses = new String[] {address}; 7 | mail.setToAddresses(toAddresses); 8 | mail.setSubject(subject); 9 | mail.setPlainTextBody(body); 10 | // Pass this email message to the built-in sendEmail method 11 | // of the Messaging class 12 | Messaging.SendEmailResult[] results = Messaging.sendEmail( 13 | new Messaging.SingleEmailMessage[] { mail }); 14 | // Call a helper method to inspect the returned results 15 | inspectResults(results); 16 | } 17 | // Helper method 18 | private static Boolean inspectResults(Messaging.SendEmailResult[] results) { 19 | Boolean sendResult = true; 20 | // sendEmail returns an array of result objects. 21 | // Iterate through the list to inspect results. 22 | // In this class, the methods send only one email, 23 | // so we should have only one result. 24 | for (Messaging.SendEmailResult res : results) { 25 | if (res.isSuccess()) { 26 | System.debug('Email sent successfully'); 27 | } 28 | else { 29 | sendResult = false; 30 | System.debug('The following errors occurred: ' + res.getErrors()); 31 | } 32 | } 33 | return sendResult; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Create a Visualforce Page/StationCheck.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    StationStatus

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | -------------------------------------------------------------------------------- /Create a Visualforce form which inserts a basic Contact record/CreateContact.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Create a Visualforce page displaying new cases/NewCaseList.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
  • 5 | 6 |

    {!Case.CaseNumber}

    7 |
    8 |
  • 9 | 10 |
    11 |
    12 | -------------------------------------------------------------------------------- /Create a Visualforce page displaying new cases/NewCaseListController.apxc: -------------------------------------------------------------------------------- 1 | public class NewCaseListController { 2 | 3 | public List getNewCases(){ 4 | List cases = [select id, CaseNumber from Case where status='New']; 5 | return cases; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Create a Visualforce page that shows user information/DisplayUserInfo.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | {! $User.FirstName} {! $User.LastName} {! $User.Username}) 4 | 5 | 6 | -------------------------------------------------------------------------------- /Create a Visualforce page which displays a variety of output fields/OppView.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Create a Visualforce page which shows a basic Contact record/ContactView.vpf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | First Name: {! Contact.FirstName}
    5 | Last Name: {! Contact.LastName}
    6 | Owner's Email: {! Contact.Owner.Email}
    7 |
    8 |
    9 |
    10 | -------------------------------------------------------------------------------- /Create a Visualforce page which shows a list of Accounts linked to their record pages/AccountList.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
  • 5 | 6 | 7 | 8 |
  • 9 |
    10 |
    11 |
    12 | -------------------------------------------------------------------------------- /Create a mobile-friendly Visualforce page using SLDS/MobileContactList.vpf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
    8 |
    {!c.Name}
    9 |
    {!c.Phone}
    10 |
    11 |
    12 |
    13 | -------------------------------------------------------------------------------- /Create a simple Visualforce page that displays an image/displayImage.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 |
    6 | 7 |
    10 |
    11 |
    12 | -------------------------------------------------------------------------------- /Create an Apex class that returns Account objects/AccountUtils.apxc: -------------------------------------------------------------------------------- 1 | public class AccountUtils { 2 | public static List accountsByState(String st) { 3 | List acctList = [SELECT Id, Name FROM Account WHERE BillingState = :st]; 4 | return acctList; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Create an Apex class that returns both contacts and leads based on a parameter/ContactAndLeadSearch.apex: -------------------------------------------------------------------------------- 1 | Public Class ContactAndLeadSearch 2 | { 3 | Public static List> searchContactsAndLeads(String searchword) 4 | { 5 | String searchQuery = 'FIND \'' + searchword + '\' IN ALL FIELDS RETURNING Lead(Name,FirstName,LastName ), Contact(FirstName,LastName )'; 6 | List> searchConLead = search.query(searchQuery); 7 | return searchConLead; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Create an Apex class that returns both contacts and leads based on a parameter/debug_console.apex: -------------------------------------------------------------------------------- 1 | List> searchContactLead = ContactAndLeadSearch.searchContactsAndLeads('Smith'); 2 | 3 | List leadList = New List(); 4 | List contList = New List(); 5 | 6 | leadList = ((List)searchContactLead[0]); 7 | contList = ((List)searchContactLead[1]); 8 | 9 | for(Lead a:leadList) 10 | { 11 | System.debug('Found following Leads ' + a.Name); 12 | } 13 | for(Contact cts:contList){ 14 | System.debug('Found following Contacts ' + cts.FirstName + '' + cts.LastName); 15 | } 16 | -------------------------------------------------------------------------------- /Create and deploy a custom big object/Rider_History__b.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | Deployed 4 | 5 | Start_Location_Lat__c 6 | 7 | false 8 | Number 9 | false 10 | 4 11 | 7 12 | 13 | 14 | Start_Location_Long__c 15 | 16 | false 17 | Number 18 | false 19 | 4 20 | 7 21 | 22 | 23 | Start_Time__c 24 | 25 | True 26 | DateTime 27 | 28 | 29 | End_Time__c 30 | 31 | false 32 | DateTime 33 | 34 | 35 | Service_Type__c 36 | 37 | 16 38 | false 39 | Text 40 | false 41 | 42 | 43 | Rider_Account__c 44 | 45 | 16 46 | true 47 | Text 48 | false 49 | 50 | 51 | Rider_Rating__c 52 | 53 | false 54 | Number 55 | false 56 | 1 57 | 2 58 | 59 | 60 | Rider_History_Index 61 | 62 | 63 | Rider_Account__c 64 | DESC 65 | 66 | 67 | Start_Time__c 68 | ASC 69 | 70 | 71 | 72 | Rider Historys 73 | 74 | -------------------------------------------------------------------------------- /Create and deploy a custom big object/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | * 5 | CustomObject 6 | 7 | 8 | * 9 | PermissionSet 10 | 11 | 41.0 12 | -------------------------------------------------------------------------------- /Create and deploy a custom big object/rider_history.permissionset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | Rider_History__b.Start_Location_Lat__c 7 | true 8 | 9 | 10 | true 11 | Rider_History__b.Start_Location_Long__c 12 | true 13 | 14 | 15 | true 16 | Rider_History__b.End_Time__c 17 | true 18 | 19 | 20 | true 21 | Rider_History__b.Service_Type__c 22 | true 23 | 24 | 25 | true 26 | Rider_History__b.Rider_Rating__c 27 | true 28 | 29 | 30 | -------------------------------------------------------------------------------- /Implement a basic Domain class and Apex trigger/Accounts.apxc: -------------------------------------------------------------------------------- 1 | public class Accounts extends fflib_SObjectDomain { 2 | public Accounts(ListsObjectList) { 3 | super(sObjectList); 4 | } 5 | 6 | 7 | public class Constructor implements fflib_SObjectDomain.IConstructable { 8 | public fflib_SObjectDomain construct(ListsObjectList) { 9 | return new Accounts(sObjectList); 10 | } 11 | } 12 | 13 | public override void onApplyDefaults() { 14 | for (Account acct:(List)Records) { 15 | acct.Description = 'Domain classes rock!'; 16 | } 17 | } 18 | 19 | public override void onBeforeUpdate(MapexistingRecords) { 20 | String rock = 'Domain classes rock!'; 21 | List updatedAccounts = new List(); 22 | for (Account acct:(List) Records) { 23 | acct.AnnualRevenue = rock.getLevenshteinDistance(acct.Description); 24 | updatedAccounts.add(acct); 25 | } 26 | 27 | fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(new Schema.SObjectType[] {Account.SObjectType}); 28 | uow.registerDirty(updatedAccounts); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Implement a basic Domain class and Apex trigger/AccountsTrigger.apxt: -------------------------------------------------------------------------------- 1 | trigger AccountsTrigger on Account (before insert, after delete, after insert, after update, after undelete, before delete, before update) { 2 | fflib_SObjectDomain.triggerHandler(Accounts.class); 3 | } 4 | -------------------------------------------------------------------------------- /Implement a basic Domain class and Apex trigger/fflib_SObjectDomain.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2012, FinancialForce.com, inc 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * - Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * - Redistributions in binary form must reproduce the above copyright notice, 11 | * this list of conditions and the following disclaimer in the documentation 12 | * and/or other materials provided with the distribution. 13 | * - Neither the name of the FinancialForce.com, inc nor the names of its contributors 14 | * may be used to endorse or promote products derived from this software without 15 | * specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 20 | * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 23 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | **/ 26 | 27 | /** 28 | * Base class aiding in the implementation of a Domain Model around SObject collections 29 | * 30 | * Domain (software engineering). “a set of common requirements, terminology, and functionality 31 | * for any software program constructed to solve a problem in that field”, 32 | * http://en.wikipedia.org/wiki/Domain_(software_engineering) 33 | * 34 | * Domain Model, “An object model of the domain that incorporates both behavior and data.”, 35 | * “At its worst business logic can be very complex. Rules and logic describe many different " 36 | * "cases and slants of behavior, and it's this complexity that objects were designed to work with...” 37 | * Martin Fowler, EAA Patterns 38 | * http://martinfowler.com/eaaCatalog/domainModel.html 39 | * 40 | **/ 41 | public virtual with sharing class fflib_SObjectDomain 42 | implements fflib_ISObjectDomain 43 | { 44 | /** 45 | * Provides access to the data represented by this domain class 46 | **/ 47 | public List Records { get; private set;} 48 | 49 | /** 50 | * Derived from the records provided during construction, provides the native describe for the standard or custom object 51 | **/ 52 | public Schema.DescribeSObjectResult SObjectDescribe {get; private set;} 53 | 54 | /** 55 | * Exposes the configuration for this domain class instance 56 | **/ 57 | public Configuration Configuration {get; private set;} 58 | 59 | /** 60 | * Useful during unit testign to assert at a more granular and robust level for errors raised during the various trigger events 61 | **/ 62 | public static ErrorFactory Errors {get; private set;} 63 | 64 | /** 65 | * Useful during unit testing to access mock support for database inserts and udpates (testing without DML) 66 | **/ 67 | public static TestFactory Test {get; private set;} 68 | 69 | /** 70 | * Retains instances of domain classes implementing trigger stateful 71 | **/ 72 | private static Map> TriggerStateByClass; 73 | 74 | /** 75 | * Retains the trigger tracking configuration used for each domain 76 | **/ 77 | private static Map TriggerEventByClass; 78 | 79 | static 80 | { 81 | Errors = new ErrorFactory(); 82 | 83 | Test = new TestFactory(); 84 | 85 | TriggerStateByClass = new Map>(); 86 | 87 | TriggerEventByClass = new Map(); 88 | } 89 | 90 | /** 91 | * Constructs the domain class with the data on which to apply the behaviour implemented within 92 | * 93 | * @param sObjectList A concrete list (e.g. List vs List) of records 94 | 95 | **/ 96 | public fflib_SObjectDomain(List sObjectList) 97 | { 98 | this(sObjectList, sObjectList.getSObjectType()); 99 | } 100 | 101 | /** 102 | * Constructs the domain class with the data and type on which to apply the behaviour implemented within 103 | * 104 | * @param sObjectList A list (e.g. List, List, etc.) of records 105 | * @param sObjectType The Schema.SObjectType of the records contained in the list 106 | * 107 | * @remark Will support List but all records in the list will be assumed to be of 108 | * the type specified in sObjectType 109 | **/ 110 | public fflib_SObjectDomain(List sObjectList, SObjectType sObjectType) 111 | { 112 | // Ensure the domain class has its own copy of the data 113 | Records = sObjectList.clone(); 114 | // Capture SObjectType describe for this domain class 115 | SObjectDescribe = sObjectType.getDescribe(); 116 | // Configure the Domain object instance 117 | Configuration = new Configuration(); 118 | } 119 | 120 | /** 121 | * Override this to apply defaults to the records, this is called by the handleBeforeInsert method 122 | **/ 123 | public virtual void onApplyDefaults() { } 124 | 125 | /** 126 | * Override this to apply general validation to be performed during insert or update, called by the handleAfterInsert and handleAfterUpdate methods 127 | **/ 128 | public virtual void onValidate() { } 129 | 130 | /** 131 | * Override this to apply validation to be performed during insert, called by the handleAfterUpdate method 132 | **/ 133 | public virtual void onValidate(Map existingRecords) { } 134 | 135 | /** 136 | * Override this to perform processing during the before insert phase, this is called by the handleBeforeInsert method 137 | **/ 138 | public virtual void onBeforeInsert() { } 139 | 140 | /** 141 | * Override this to perform processing during the before update phase, this is called by the handleBeforeUpdate method 142 | **/ 143 | public virtual void onBeforeUpdate(Map existingRecords) { } 144 | 145 | /** 146 | * Override this to perform processing during the before delete phase, this is called by the handleBeforeDelete method 147 | **/ 148 | public virtual void onBeforeDelete() { } 149 | 150 | /** 151 | * Override this to perform processing during the after insert phase, this is called by the handleAfterInsert method 152 | **/ 153 | public virtual void onAfterInsert() { } 154 | 155 | /** 156 | * Override this to perform processing during the after update phase, this is called by the handleAfterUpdate method 157 | **/ 158 | public virtual void onAfterUpdate(Map existingRecords) { } 159 | 160 | /** 161 | * Override this to perform processing during the after delete phase, this is called by the handleAfterDelete method 162 | **/ 163 | public virtual void onAfterDelete() { } 164 | 165 | /** 166 | * Override this to perform processing during the after undelete phase, this is called by the handleAfterDelete method 167 | **/ 168 | public virtual void onAfterUndelete() { } 169 | 170 | /** 171 | * Base handler for the Apex Trigger event Before Insert, calls the onApplyDefaults method, followed by onBeforeInsert 172 | **/ 173 | public virtual void handleBeforeInsert() 174 | { 175 | onApplyDefaults(); 176 | onBeforeInsert(); 177 | } 178 | 179 | /** 180 | * Base handler for the Apex Trigger event Before Update, calls the onBeforeUpdate method 181 | **/ 182 | public virtual void handleBeforeUpdate(Map existingRecords) 183 | { 184 | onBeforeUpdate(existingRecords); 185 | } 186 | 187 | /** 188 | * Base handler for the Apex Trigger event Before Delete, calls the onBeforeDelete method 189 | **/ 190 | public virtual void handleBeforeDelete() 191 | { 192 | onBeforeDelete(); 193 | } 194 | 195 | /** 196 | * Base handler for the Apex Trigger event After Insert, checks object security and calls the onValidate and onAfterInsert methods 197 | * 198 | * @throws DomainException if the current user context is not able to create records 199 | **/ 200 | public virtual void handleAfterInsert() 201 | { 202 | if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isCreateable()) 203 | throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.'); 204 | 205 | onValidate(); 206 | onAfterInsert(); 207 | } 208 | 209 | /** 210 | * Base handler for the Apex Trigger event After Update, checks object security and calls the onValidate, onValidate(Map) and onAfterUpdate methods 211 | * 212 | * @throws DomainException if the current user context is not able to update records 213 | **/ 214 | public virtual void handleAfterUpdate(Map existingRecords) 215 | { 216 | if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isUpdateable()) 217 | throw new DomainException('Permission to udpate an ' + SObjectDescribe.getName() + ' denied.'); 218 | 219 | if(Configuration.OldOnUpdateValidateBehaviour) 220 | onValidate(); 221 | onValidate(existingRecords); 222 | onAfterUpdate(existingRecords); 223 | } 224 | 225 | /** 226 | * Base handler for the Apex Trigger event After Delete, checks object security and calls the onAfterDelete method 227 | * 228 | * @throws DomainException if the current user context is not able to delete records 229 | **/ 230 | public virtual void handleAfterDelete() 231 | { 232 | if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isDeletable()) 233 | throw new DomainException('Permission to delete an ' + SObjectDescribe.getName() + ' denied.'); 234 | 235 | onAfterDelete(); 236 | } 237 | 238 | /** 239 | * Base handler for the Apex Trigger event After Undelete, checks object security and calls the onAfterUndelete method 240 | * 241 | * @throws DomainException if the current user context is not able to delete records 242 | **/ 243 | public virtual void handleAfterUndelete() 244 | { 245 | if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isCreateable()) 246 | throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.'); 247 | 248 | onAfterUndelete(); 249 | } 250 | 251 | /** 252 | * Returns the SObjectType this Domain class represents 253 | **/ 254 | public SObjectType getSObjectType() 255 | { 256 | return SObjectDescribe.getSObjectType(); 257 | } 258 | 259 | /** 260 | * Returns the SObjectType this Domain class represents 261 | **/ 262 | public SObjectType sObjectType() 263 | { 264 | return getSObjectType(); 265 | } 266 | 267 | /** 268 | * Alternative to the Records property, provided to support mocking of Domain classes 269 | **/ 270 | public List getRecords() 271 | { 272 | return Records; 273 | } 274 | 275 | /** 276 | * Detects whether any values in context records have changed for given fields as strings 277 | * Returns list of SObject records that have changes in the specified fields 278 | **/ 279 | public List getChangedRecords(Set fieldNames) 280 | { 281 | List changedRecords = new List(); 282 | for(SObject newRecord : Records) 283 | { 284 | Id recordId = (Id)newRecord.get('Id'); 285 | if(Trigger.oldMap == null || !Trigger.oldMap.containsKey(recordId)) continue; 286 | SObject oldRecord = Trigger.oldMap.get(recordId); 287 | for(String fieldName : fieldNames) 288 | { 289 | if(oldRecord.get(fieldName) != newRecord.get(fieldName)) changedRecords.add(newRecord); 290 | } 291 | } 292 | return changedRecords; 293 | } 294 | 295 | /** 296 | * Detects whether any values in context records have changed for given fields as tokens 297 | * Returns list of SObject records that have changes in the specified fields 298 | **/ 299 | public List getChangedRecords(Set fieldTokens) 300 | { 301 | List changedRecords = new List(); 302 | for(SObject newRecord : Records) 303 | { 304 | Id recordId = (Id)newRecord.get('Id'); 305 | if(Trigger.oldMap == null || !Trigger.oldMap.containsKey(recordId)) continue; 306 | SObject oldRecord = Trigger.oldMap.get(recordId); 307 | for(Schema.SObjectField fieldToken : fieldTokens) 308 | { 309 | if(oldRecord.get(fieldToken) != newRecord.get(fieldToken)) changedRecords.add(newRecord); 310 | } 311 | } 312 | return changedRecords; 313 | } 314 | 315 | /** 316 | * Interface used to aid the triggerHandler in constructing instances of Domain classes 317 | **/ 318 | public interface IConstructable 319 | { 320 | fflib_SObjectDomain construct(List sObjectList); 321 | } 322 | 323 | /** 324 | * Interface used to aid the triggerHandler in constructing instances of Domain classes 325 | **/ 326 | public interface IConstructable2 extends IConstructable 327 | { 328 | fflib_SObjectDomain construct(List sObjectList, SObjectType sObjectType); 329 | } 330 | 331 | /** 332 | * For Domain classes implementing the ITriggerStateful interface returns the instance 333 | * of the domain class being shared between trigger invocations, returns null if 334 | * the Domain class trigger has not yet fired or the given domain class does not implement 335 | * the ITriggerStateful interface. Note this method is sensitive to recursion, meaning 336 | * it will return the applicable domain instance for the level of recursion 337 | **/ 338 | public static fflib_SObjectDomain getTriggerInstance(Type domainClass) 339 | { 340 | List domains = TriggerStateByClass.get(domainClass); 341 | if(domains==null || domains.size()==0) 342 | return null; 343 | return domains[domains.size()-1]; 344 | } 345 | 346 | /** 347 | * Method constructs the given Domain class with the current Trigger context 348 | * before calling the applicable override methods such as beforeInsert, beforeUpdate etc. 349 | **/ 350 | public static void triggerHandler(Type domainClass) 351 | { 352 | // Process the trigger context 353 | if(System.Test.isRunningTest() & Test.Database.hasRecords()) 354 | { 355 | // If in test context and records in the mock database delegate initially to the mock database trigger handler 356 | Test.Database.testTriggerHandler(domainClass); 357 | } 358 | else 359 | { 360 | // Process the runtime Apex Trigger context 361 | triggerHandler(domainClass, 362 | Trigger.isBefore, 363 | Trigger.isAfter, 364 | Trigger.isInsert, 365 | Trigger.isUpdate, 366 | Trigger.isDelete, 367 | Trigger.isUnDelete, 368 | Trigger.new, 369 | Trigger.oldMap); 370 | } 371 | } 372 | 373 | /** 374 | * Calls the applicable override methods such as beforeInsert, beforeUpdate etc. based on a Trigger context 375 | **/ 376 | private static void triggerHandler(Type domainClass, Boolean isBefore, Boolean isAfter, Boolean isInsert, Boolean isUpdate, Boolean isDelete, Boolean isUndelete, List newRecords, Map oldRecordsMap) 377 | { 378 | // After phase of trigger will reuse prior instance of domain class if ITriggerStateful implemented 379 | fflib_SObjectDomain domainObject = isBefore ? null : popTriggerInstance(domainClass, isDelete ? oldRecordsMap.values() : newRecords); 380 | if(domainObject==null) 381 | { 382 | // Construct the domain class constructor class 383 | String domainClassName = domainClass.getName(); 384 | Type constructableClass = domainClassName.endsWith('Constructor') ? Type.forName(domainClassName) : Type.forName(domainClassName+'.Constructor'); 385 | IConstructable domainConstructor = (IConstructable) constructableClass.newInstance(); 386 | 387 | // Construct the domain class with the approprite record set 388 | if(isInsert) domainObject = domainConstructor.construct(newRecords); 389 | else if(isUpdate) domainObject = domainConstructor.construct(newRecords); 390 | else if(isDelete) domainObject = domainConstructor.construct(oldRecordsMap.values()); 391 | else if(isUndelete) domainObject = domainConstructor.construct(newRecords); 392 | 393 | // Should this instance be reused on the next trigger invocation? 394 | if(domainObject.Configuration.TriggerStateEnabled) 395 | // Push this instance onto the stack to be popped during the after phase 396 | pushTriggerInstance(domainClass, domainObject); 397 | } 398 | 399 | // has this event been disabled? 400 | if(!getTriggerEvent(domainClass).isEnabled(isBefore, isAfter, isInsert, isUpdate, isDelete, isUndelete)) 401 | { 402 | return; 403 | } 404 | 405 | // Invoke the applicable handler 406 | if(isBefore) 407 | { 408 | if(isInsert) domainObject.handleBeforeInsert(); 409 | else if(isUpdate) domainObject.handleBeforeUpdate(oldRecordsMap); 410 | else if(isDelete) domainObject.handleBeforeDelete(); 411 | } 412 | else 413 | { 414 | if(isInsert) domainObject.handleAfterInsert(); 415 | else if(isUpdate) domainObject.handleAfterUpdate(oldRecordsMap); 416 | else if(isDelete) domainObject.handleAfterDelete(); 417 | else if(isUndelete) domainObject.handleAfterUndelete(); 418 | } 419 | } 420 | 421 | /** 422 | * Pushes to the stack of domain classes per type a domain object instance 423 | **/ 424 | private static void pushTriggerInstance(Type domainClass, fflib_SObjectDomain domain) 425 | { 426 | List domains = TriggerStateByClass.get(domainClass); 427 | if(domains==null) 428 | TriggerStateByClass.put(domainClass, domains = new List()); 429 | domains.add(domain); 430 | } 431 | 432 | /** 433 | * Pops from the stack of domain classes per type a domain object instance and updates the record set 434 | **/ 435 | private static fflib_SObjectDomain popTriggerInstance(Type domainClass, List records) 436 | { 437 | List domains = TriggerStateByClass.get(domainClass); 438 | if(domains==null || domains.size()==0) 439 | return null; 440 | fflib_SObjectDomain domain = domains.remove(domains.size()-1); 441 | domain.Records = records; 442 | return domain; 443 | } 444 | 445 | public static TriggerEvent getTriggerEvent(Type domainClass) 446 | { 447 | if(!TriggerEventByClass.containsKey(domainClass)) 448 | { 449 | TriggerEventByClass.put(domainClass, new TriggerEvent()); 450 | } 451 | 452 | return TriggerEventByClass.get(domainClass); 453 | } 454 | 455 | public class TriggerEvent 456 | { 457 | public boolean BeforeInsertEnabled {get; private set;} 458 | public boolean BeforeUpdateEnabled {get; private set;} 459 | public boolean BeforeDeleteEnabled {get; private set;} 460 | 461 | public boolean AfterInsertEnabled {get; private set;} 462 | public boolean AfterUpdateEnabled {get; private set;} 463 | public boolean AfterDeleteEnabled {get; private set;} 464 | public boolean AfterUndeleteEnabled {get; private set;} 465 | 466 | public TriggerEvent() 467 | { 468 | this.enableAll(); 469 | } 470 | 471 | // befores 472 | public TriggerEvent enableBeforeInsert() {BeforeInsertEnabled = true; return this;} 473 | public TriggerEvent enableBeforeUpdate() {BeforeUpdateEnabled = true; return this;} 474 | public TriggerEvent enableBeforeDelete() {BeforeDeleteEnabled = true; return this;} 475 | 476 | public TriggerEvent disableBeforeInsert() {BeforeInsertEnabled = false; return this;} 477 | public TriggerEvent disableBeforeUpdate() {BeforeUpdateEnabled = false; return this;} 478 | public TriggerEvent disableBeforeDelete() {BeforeDeleteEnabled = false; return this;} 479 | 480 | // afters 481 | public TriggerEvent enableAfterInsert() {AfterInsertEnabled = true; return this;} 482 | public TriggerEvent enableAfterUpdate() {AfterUpdateEnabled = true; return this;} 483 | public TriggerEvent enableAfterDelete() {AfterDeleteEnabled = true; return this;} 484 | public TriggerEvent enableAfterUndelete() {AfterUndeleteEnabled = true; return this;} 485 | 486 | 487 | public TriggerEvent disableAfterInsert() {AfterInsertEnabled = false; return this;} 488 | public TriggerEvent disableAfterUpdate() {AfterUpdateEnabled = false; return this;} 489 | public TriggerEvent disableAfterDelete() {AfterDeleteEnabled = false; return this;} 490 | public TriggerEvent disableAfterUndelete(){AfterUndeleteEnabled = false; return this;} 491 | 492 | public TriggerEvent enableAll() 493 | { 494 | return this.enableAllBefore().enableAllAfter(); 495 | } 496 | 497 | public TriggerEvent disableAll() 498 | { 499 | return this.disableAllBefore().disableAllAfter(); 500 | } 501 | 502 | public TriggerEvent enableAllBefore() 503 | { 504 | return this.enableBeforeInsert().enableBeforeUpdate().enableBeforeDelete(); 505 | } 506 | 507 | public TriggerEvent disableAllBefore() 508 | { 509 | return this.disableBeforeInsert().disableBeforeUpdate().disableBeforeDelete(); 510 | } 511 | 512 | public TriggerEvent enableAllAfter() 513 | { 514 | return this.enableAfterInsert().enableAfterUpdate().enableAfterDelete().enableAfterUndelete(); 515 | } 516 | 517 | public TriggerEvent disableAllAfter() 518 | { 519 | return this.disableAfterInsert().disableAfterUpdate().disableAfterDelete().disableAfterUndelete(); 520 | } 521 | 522 | public boolean isEnabled(Boolean isBefore, Boolean isAfter, Boolean isInsert, Boolean isUpdate, Boolean isDelete, Boolean isUndelete) 523 | { 524 | if(isBefore) 525 | { 526 | if(isInsert) return BeforeInsertEnabled; 527 | else if(isUpdate) return BeforeUpdateEnabled; 528 | else if(isDelete) return BeforeDeleteEnabled; 529 | } 530 | else if(isAfter) 531 | { 532 | if(isInsert) return AfterInsertEnabled; 533 | else if(isUpdate) return AfterUpdateEnabled; 534 | else if(isDelete) return AfterDeleteEnabled; 535 | else if(isUndelete) return AfterUndeleteEnabled; 536 | } 537 | return true; // shouldnt ever get here! 538 | } 539 | } 540 | 541 | /** 542 | * Fluent style Configuration system for Domain class creation 543 | **/ 544 | public class Configuration 545 | { 546 | /** 547 | * Backwards compatibility mode for handleAfterUpdate routing to onValidate() 548 | **/ 549 | public Boolean OldOnUpdateValidateBehaviour {get; private set;} 550 | /** 551 | * True if the base class is checking the users CRUD requirements before invoking trigger methods 552 | **/ 553 | public Boolean EnforcingTriggerCRUDSecurity {get; private set;} 554 | 555 | /** 556 | * Enables reuse of the same Domain instance between before and after trigger phases (subject to recursive scenarios) 557 | **/ 558 | public Boolean TriggerStateEnabled {get; private set;} 559 | 560 | /** 561 | * Default configuration 562 | **/ 563 | public Configuration() 564 | { 565 | EnforcingTriggerCRUDSecurity = true; // Default is true for backwards compatability 566 | TriggerStateEnabled = false; 567 | OldOnUpdateValidateBehaviour = false; // Breaking change, but felt to better practice 568 | } 569 | 570 | /** 571 | * See associated property 572 | **/ 573 | public Configuration enableTriggerState() 574 | { 575 | TriggerStateEnabled = true; 576 | return this; 577 | } 578 | 579 | /** 580 | * See associated property 581 | **/ 582 | public Configuration disableTriggerState() 583 | { 584 | TriggerStateEnabled = false; 585 | return this; 586 | } 587 | 588 | /** 589 | * See associated property 590 | **/ 591 | public Configuration enforceTriggerCRUDSecurity() 592 | { 593 | EnforcingTriggerCRUDSecurity = true; 594 | return this; 595 | } 596 | 597 | /** 598 | * See associated property 599 | **/ 600 | public Configuration disableTriggerCRUDSecurity() 601 | { 602 | EnforcingTriggerCRUDSecurity = false; 603 | return this; 604 | } 605 | 606 | /** 607 | * See associated property 608 | **/ 609 | public Configuration enableOldOnUpdateValidateBehaviour() 610 | { 611 | OldOnUpdateValidateBehaviour = true; 612 | return this; 613 | } 614 | 615 | /** 616 | * See associated property 617 | **/ 618 | public Configuration disableOldOnUpdateValidateBehaviour() 619 | { 620 | OldOnUpdateValidateBehaviour = false; 621 | return this; 622 | } 623 | } 624 | 625 | /** 626 | * General exception class for the domain layer 627 | **/ 628 | public class DomainException extends Exception 629 | { 630 | } 631 | 632 | /** 633 | * Ensures logging of errors in the Domain context for later assertions in tests 634 | **/ 635 | public String error(String message, SObject record) 636 | { 637 | return Errors.error(this, message, record); 638 | } 639 | 640 | /** 641 | * Ensures logging of errors in the Domain context for later assertions in tests 642 | **/ 643 | public String error(String message, SObject record, SObjectField field) 644 | { 645 | return Errors.error(this, message, record, field); 646 | } 647 | 648 | /** 649 | * Ensures logging of errors in the Domain context for later assertions in tests 650 | **/ 651 | public class ErrorFactory 652 | { 653 | private List errorList = new List(); 654 | 655 | private ErrorFactory() 656 | { 657 | 658 | } 659 | 660 | public String error(String message, SObject record) 661 | { 662 | return error(null, message, record); 663 | } 664 | 665 | private String error(fflib_SObjectDomain domain, String message, SObject record) 666 | { 667 | ObjectError objectError = new ObjectError(); 668 | objectError.domain = domain; 669 | objectError.message = message; 670 | objectError.record = record; 671 | errorList.add(objectError); 672 | return message; 673 | } 674 | 675 | public String error(String message, SObject record, SObjectField field) 676 | { 677 | return error(null, message, record, field); 678 | } 679 | 680 | private String error(fflib_SObjectDomain domain, String message, SObject record, SObjectField field) 681 | { 682 | FieldError fieldError = new FieldError(); 683 | fieldError.domain = domain; 684 | fieldError.message = message; 685 | fieldError.record = record; 686 | fieldError.field = field; 687 | errorList.add(fieldError); 688 | return message; 689 | } 690 | 691 | public List getAll() 692 | { 693 | return errorList.clone(); 694 | } 695 | 696 | public void clearAll() 697 | { 698 | errorList.clear(); 699 | } 700 | } 701 | 702 | /** 703 | * Ensures logging of errors in the Domain context for later assertions in tests 704 | **/ 705 | public virtual class FieldError extends ObjectError 706 | { 707 | public SObjectField field; 708 | 709 | public FieldError() 710 | { 711 | 712 | } 713 | } 714 | 715 | /** 716 | * Ensures logging of errors in the Domain context for later assertions in tests 717 | **/ 718 | public virtual class ObjectError extends Error 719 | { 720 | public SObject record; 721 | 722 | public ObjectError() 723 | { 724 | 725 | } 726 | } 727 | 728 | /** 729 | * Ensures logging of errors in the Domain context for later assertions in tests 730 | **/ 731 | public abstract class Error 732 | { 733 | public String message; 734 | public fflib_SObjectDomain domain; 735 | } 736 | 737 | /** 738 | * Provides test context mocking facilities to unit tests testing domain classes 739 | **/ 740 | public class TestFactory 741 | { 742 | public MockDatabase Database = new MockDatabase(); 743 | 744 | private TestFactory() 745 | { 746 | 747 | } 748 | } 749 | 750 | /** 751 | * Class used during Unit testing of Domain classes, can be used (not exclusively) to speed up test execution and focus testing 752 | **/ 753 | public class MockDatabase 754 | { 755 | private Boolean isInsert = false; 756 | private Boolean isUpdate = false; 757 | private Boolean isDelete = false; 758 | private Boolean isUndelete = false; 759 | private List records = new List(); 760 | private Map oldRecords = new Map(); 761 | 762 | private MockDatabase() 763 | { 764 | 765 | } 766 | 767 | private void testTriggerHandler(Type domainClass) 768 | { 769 | // Mock Before 770 | triggerHandler(domainClass, true, false, isInsert, isUpdate, isDelete, isUndelete, records, oldRecords); 771 | 772 | // Mock After 773 | triggerHandler(domainClass, false, true, isInsert, isUpdate, isDelete, isUndelete, records, oldRecords); 774 | } 775 | 776 | public void onInsert(List records) 777 | { 778 | this.isInsert = true; 779 | this.isUpdate = false; 780 | this.isDelete = false; 781 | this.isUndelete = false; 782 | this.records = records; 783 | } 784 | 785 | public void onUpdate(List records, Map oldRecords) 786 | { 787 | this.isInsert = false; 788 | this.isUpdate = true; 789 | this.isDelete = false; 790 | this.records = records; 791 | this.isUndelete = false; 792 | this.oldRecords = oldRecords; 793 | } 794 | 795 | public void onDelete(Map records) 796 | { 797 | this.isInsert = false; 798 | this.isUpdate = false; 799 | this.isDelete = true; 800 | this.isUndelete = false; 801 | this.oldRecords = records; 802 | } 803 | 804 | public void onUndelete(List records) 805 | { 806 | this.isInsert = false; 807 | this.isUpdate = false; 808 | this.isDelete = false; 809 | this.isUndelete = true; 810 | this.records = records; 811 | } 812 | 813 | public Boolean hasRecords() 814 | { 815 | return records!=null && records.size()>0 || oldRecords!=null && oldRecords.size()>0; 816 | } 817 | } 818 | 819 | /** 820 | * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes) 821 | **/ 822 | public with sharing class TestSObjectDomain extends fflib_SObjectDomain 823 | { 824 | private String someState; 825 | 826 | public TestSObjectDomain(List sObjectList) 827 | { 828 | // Domain classes are initialised with lists to enforce bulkification throughout 829 | super(sObjectList); 830 | } 831 | 832 | public TestSObjectDomain(List sObjectList, SObjectType sObjectType) 833 | { 834 | // Domain classes are initialised with lists to enforce bulkification throughout 835 | super(sObjectList, sObjectType); 836 | } 837 | 838 | public override void onApplyDefaults() 839 | { 840 | // Not required in production code 841 | super.onApplyDefaults(); 842 | 843 | // Apply defaults to Testfflib_SObjectDomain 844 | for(Opportunity opportunity : (List) Records) 845 | { 846 | opportunity.CloseDate = System.today().addDays(30); 847 | } 848 | } 849 | 850 | public override void onValidate() 851 | { 852 | // Not required in production code 853 | super.onValidate(); 854 | 855 | // Validate Testfflib_SObjectDomain 856 | for(Opportunity opp : (List) Records) 857 | { 858 | if(opp.Type!=null && opp.Type.startsWith('Existing') && opp.AccountId == null) 859 | { 860 | opp.AccountId.addError( error('You must provide an Account for Opportunities for existing Customers.', opp, Opportunity.AccountId) ); 861 | } 862 | } 863 | } 864 | 865 | public override void onValidate(Map existingRecords) 866 | { 867 | // Not required in production code 868 | super.onValidate(existingRecords); 869 | 870 | // Validate changes to Testfflib_SObjectDomain 871 | for(Opportunity opp : (List) Records) 872 | { 873 | Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id); 874 | if(opp.Type != existingOpp.Type) 875 | { 876 | opp.Type.addError( error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) ); 877 | } 878 | } 879 | } 880 | 881 | public override void onBeforeDelete() 882 | { 883 | // Not required in production code 884 | super.onBeforeDelete(); 885 | 886 | // Validate changes to Testfflib_SObjectDomain 887 | for(Opportunity opp : (List) Records) 888 | { 889 | opp.addError( error('You cannot delete this Opportunity.', opp) ); 890 | } 891 | } 892 | 893 | public override void onAfterUndelete() 894 | { 895 | // Not required in production code 896 | super.onAfterUndelete(); 897 | } 898 | 899 | public override void onBeforeInsert() 900 | { 901 | // Assert this variable is null in the after insert (since this domain class is stateless) 902 | someState = 'This should not survice the trigger after phase'; 903 | } 904 | 905 | public override void onAfterInsert() 906 | { 907 | // This is a stateless domain class, so should not retain anything betweet before and after 908 | System.assertEquals(null, someState); 909 | } 910 | } 911 | 912 | /** 913 | * Typically an inner class to the domain class, supported here for test purposes 914 | **/ 915 | public class TestSObjectDomainConstructor implements fflib_SObjectDomain.IConstructable 916 | { 917 | public fflib_SObjectDomain construct(List sObjectList) 918 | { 919 | return new TestSObjectDomain(sObjectList); 920 | } 921 | } 922 | 923 | /** 924 | * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes) 925 | **/ 926 | public with sharing class TestSObjectStatefulDomain 927 | extends fflib_SObjectDomain 928 | { 929 | public String someState; 930 | 931 | public TestSObjectStatefulDomain(List sObjectList) 932 | { 933 | super(sObjectList); 934 | 935 | // Ensure this instance is re-used in the after trigger phase (subject to recursive scenarios) 936 | Configuration.enableTriggerState(); 937 | } 938 | 939 | public override void onBeforeInsert() 940 | { 941 | // This must always be null, as we do not reuse domain instances within recursive scenarios (different record sets) 942 | System.assertEquals(null, someState); 943 | 944 | // Process records 945 | List newOpps = new List(); 946 | for(Opportunity opp : (List) Records) 947 | { 948 | // Set some state sensitive to the incoming records 949 | someState = 'Error on Record ' + opp.Name; 950 | 951 | // Create a new Opportunity record to trigger recursive code path? 952 | if(opp.Name.equals('Test Recursive 1')) 953 | newOpps.add(new Opportunity ( Name = 'Test Recursive 2', Type = 'Existing Account' )); 954 | } 955 | 956 | // If testing recursiving emulate an insert 957 | if(newOpps.size()>0) 958 | { 959 | // This will force recursion and thus validate via the above assert results in a new domain instance 960 | fflib_SObjectDomain.Test.Database.onInsert(newOpps); 961 | fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectStatefulDomainConstructor.class); 962 | } 963 | } 964 | 965 | public override void onAfterInsert() 966 | { 967 | // Use the state set in the before insert (since this is a stateful domain class) 968 | if(someState!=null) 969 | for(Opportunity opp : (List) Records) 970 | opp.addError(error(someState, opp)); 971 | } 972 | } 973 | 974 | /** 975 | * Typically an inner class to the domain class, supported here for test purposes 976 | **/ 977 | public class TestSObjectStatefulDomainConstructor implements fflib_SObjectDomain.IConstructable 978 | { 979 | public fflib_SObjectDomain construct(List sObjectList) 980 | { 981 | return new TestSObjectStatefulDomain(sObjectList); 982 | } 983 | } 984 | 985 | /** 986 | * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes) 987 | **/ 988 | public with sharing class TestSObjectOnValidateBehaviour 989 | extends fflib_SObjectDomain 990 | { 991 | public TestSObjectOnValidateBehaviour(List sObjectList) 992 | { 993 | super(sObjectList); 994 | 995 | // Enable old behaviour based on the test Opportunity name passed in 996 | if(sObjectList[0].Name == 'Test Enable Old Behaviour') 997 | Configuration.enableOldOnUpdateValidateBehaviour(); 998 | } 999 | 1000 | public override void onValidate() 1001 | { 1002 | // Throw exception to give the test somethign to assert on 1003 | throw new DomainException('onValidate called'); 1004 | } 1005 | } 1006 | 1007 | /** 1008 | * Typically an inner class to the domain class, supported here for test purposes 1009 | **/ 1010 | public class TestSObjectOnValidateBehaviourConstructor implements fflib_SObjectDomain.IConstructable 1011 | { 1012 | public fflib_SObjectDomain construct(List sObjectList) 1013 | { 1014 | return new TestSObjectOnValidateBehaviour(sObjectList); 1015 | } 1016 | } 1017 | 1018 | /** 1019 | * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes) 1020 | **/ 1021 | public with sharing class TestSObjectChangedRecords 1022 | extends fflib_SObjectDomain 1023 | { 1024 | public TestSObjectChangedRecords(List sObjectList) 1025 | { 1026 | super(sObjectList); 1027 | } 1028 | } 1029 | 1030 | /** 1031 | * Typically an inner class to the domain class, supported here for test purposes 1032 | **/ 1033 | public class TestSObjectChangedRecordsConstructor implements fflib_SObjectDomain.IConstructable 1034 | { 1035 | public fflib_SObjectDomain construct(List sObjectList) 1036 | { 1037 | return new TestSObjectChangedRecords(sObjectList); 1038 | } 1039 | } 1040 | 1041 | /** 1042 | * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes) 1043 | **/ 1044 | public with sharing class TestSObjectDisableBehaviour 1045 | extends fflib_SObjectDomain 1046 | { 1047 | public TestSObjectDisableBehaviour(List sObjectList) 1048 | { 1049 | super(sObjectList); 1050 | } 1051 | 1052 | public override void onAfterInsert() { 1053 | // Throw exception to give the test somethign to assert on 1054 | throw new DomainException('onAfterInsert called'); 1055 | } 1056 | 1057 | public override void onBeforeInsert() { 1058 | // Throw exception to give the test somethign to assert on 1059 | throw new DomainException('onBeforeInsert called'); 1060 | } 1061 | 1062 | public override void onAfterUpdate(map existing) { 1063 | // Throw exception to give the test somethign to assert on 1064 | throw new DomainException('onAfterUpdate called'); 1065 | } 1066 | 1067 | public override void onBeforeUpdate(map existing) { 1068 | // Throw exception to give the test somethign to assert on 1069 | throw new DomainException('onBeforeUpdate called'); 1070 | } 1071 | 1072 | public override void onAfterDelete() { 1073 | // Throw exception to give the test somethign to assert on 1074 | throw new DomainException('onAfterDelete called'); 1075 | } 1076 | 1077 | public override void onBeforeDelete() { 1078 | // Throw exception to give the test somethign to assert on 1079 | throw new DomainException('onBeforeDelete called'); 1080 | } 1081 | 1082 | public override void onAfterUndelete() { 1083 | // Throw exception to give the test somethign to assert on 1084 | throw new DomainException('onAfterUndelete called'); 1085 | } 1086 | } 1087 | 1088 | /** 1089 | * Typically an inner class to the domain class, supported here for test purposes 1090 | **/ 1091 | public class TestSObjectDisableBehaviourConstructor implements fflib_SObjectDomain.IConstructable 1092 | { 1093 | public fflib_SObjectDomain construct(List sObjectList) 1094 | { 1095 | return new TestSObjectDisableBehaviour(sObjectList); 1096 | } 1097 | } 1098 | } 1099 | -------------------------------------------------------------------------------- /Refactor Components and Communicate with Events/README.md: -------------------------------------------------------------------------------- 1 | ## Refactor Components and Communicate with Events 2 | 3 | * Refactor the input form for camping list items into its own component and communicate with component events. 4 | * Replace the HTML form in the campingList component with a new campingListForm component that calls the clickCreateItem JavaScript controller action when clicked. 5 | * The campingList component listens for a c:addItemEvent event and executes the action handleAddItem in the JavaScript controller. The handleAdditem method saves the record to the database and adds the record to the items value provider. 6 | * The addItemEvent event is of type component and has a Camping_Item__c type attribute named item. 7 | * The campingListForm registers an addItem event of type c:addItemEvent. 8 | * The campingListFormController JavaScript controller calls the helper's createItem method if the form is valid. 9 | * The campingListFormHelper JavaScript helper creates an addItem event with the item to be added and then fires the event. It then resets the newItem value provider with a blank sObjectType of type Camping_Item__c. 10 | -------------------------------------------------------------------------------- /Refactor Components and Communicate with Events/addItemEvent.evt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Refactor Components and Communicate with Events/campingList.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 |
      13 |
    1. Bug Spray
    2. 14 |
    3. Bear Repellant
    4. 15 |
    5. Goat Food
    6. 16 |
    17 | 18 | 19 |
    20 | 21 | 22 | 23 |
    24 | 25 | 26 | 27 |
    28 |
    29 |

    Items

    30 |
    31 | 32 |
    33 |
    34 | 35 | 36 | 37 |
    38 |
    39 |
    40 | 41 |
    42 | -------------------------------------------------------------------------------- /Refactor Components and Communicate with Events/campingListController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | // Load items from Salesforce 3 | doInit: function(component, event, helper) { 4 | 5 | // Create the action 6 | var action = component.get("c.getItems"); 7 | 8 | // Add callback behavior for when response is received 9 | action.setCallback(this, function(response) { 10 | var state = response.getState(); 11 | if (component.isValid() && state === "SUCCESS") { 12 | component.set("v.items", response.getReturnValue()); 13 | } 14 | else { 15 | console.log("Failed with state: " + state); 16 | } 17 | }); 18 | 19 | // Send action off to be executed 20 | $A.enqueueAction(action); 21 | }, 22 | 23 | handleAddItem: function(component, event, helper) { 24 | // var newItem = event.getParam("item"); 25 | //helper.addItem(component, newItem); 26 | var action = component.get("c.saveItem"); 27 | action.setParams({"item": newItem}); 28 | action.setCallback(this, function(response){ 29 | var state = response.getState(); 30 | if (component.isValid() && state === "SUCCESS") { 31 | // all good, nothing to do. 32 | var items = component.get("v.items"); 33 | items.push(response.getReturnValue()); 34 | component.set("v.items", items); 35 | } 36 | }); 37 | $A.enqueueAction(action); 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /Refactor Components and Communicate with Events/campingListForm.cpm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 |
    12 | 13 |
    14 |
    15 | 20 | 21 |
    22 |
    23 | 24 |
    25 |
    26 | 31 | 32 |
    33 |
    34 | 35 |
    36 |
    37 | 42 |
    43 |
    44 | 45 |
    46 | 50 |
    51 | 52 |
    53 | 56 |
    57 | 58 |
    59 | 60 |
    61 | 62 | -------------------------------------------------------------------------------- /Refactor Components and Communicate with Events/campingListFormController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | 3 | submitForm: function(component, event, helper) { 4 | if(helper.validateItemForm(component)){ 5 | // Create the new item 6 | var newItem = component.get("v.newItem"); 7 | helper.createItem(component, newItem); 8 | } 9 | 10 | } 11 | 12 | }) 13 | -------------------------------------------------------------------------------- /Refactor Components and Communicate with Events/campingListFormHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | addItem: function(component, newItem) { 3 | var addItem = component.getItem("addItem"); 4 | addItem.setParams({ "item": item }); 5 | addItem.fire(); 6 | component.set("v.newItem",{ 'sobjectType': 'Camping_Item__c', 7 | 'Name': '', 8 | 'Quantity__c': 0, 9 | 'Price__c': 0, 10 | 'Packed__c': false } />); 11 | }, 12 | 13 | 14 | validateItemForm: function(component) { 15 | 16 | // Simplistic error checking 17 | var validItem = true; 18 | 19 | // Name must not be blank 20 | var nameField = component.find("itemname"); 21 | var itemname = nameField.get("v.value"); 22 | if ($A.util.isEmpty(itemname)){ 23 | validItem = false; 24 | nameField.set("v.errors", [{message:"Item name can't be blank."}]); 25 | } 26 | else { 27 | nameField.set("v.errors", null); 28 | } 29 | 30 | // Quantity must not be blank 31 | var quantityField = component.find("quantity"); 32 | var quantity = nameField.get("v.value"); 33 | if ($A.util.isEmpty(quantity)){ 34 | validItem = false; 35 | quantityField.set("v.errors", [{message:"Quantity can't be blank."}]); 36 | } 37 | else { 38 | quantityField.set("v.errors", null); 39 | } 40 | // Price must not be blank 41 | var priceField = component.find("price"); 42 | var price = priceField.get("v.value"); 43 | if ($A.util.isEmpty(price)){ 44 | validItem = false; 45 | priceField.set("v.errors", [{message:"Price can't be blank."}]); 46 | } 47 | else { 48 | quantityField.set("v.errors", null); 49 | } 50 | return (validItem); 51 | 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /Refactor Components and Communicate with Events/campingListHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | addItem: function(component, item) { 3 | this.saveItem(component, item, function(response){ 4 | var state = response.getState(); 5 | if (component.isValid() && state === "SUCCESS") { 6 | // all good, nothing to do. 7 | /* var items = component.get("v.items"); 8 | items.push(response.getReturnValue()); 9 | component.set("v.items", items);*/ 10 | } 11 | }); 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /Save and Load Records with a Server-Side Controller/CampingListController.cls: -------------------------------------------------------------------------------- 1 | public with sharing class CampingListController { 2 | 3 | /** 4 | * This method will insert the camping item record 5 | * 6 | */ 7 | @AuraEnabled 8 | public static void saveItem (Camping_Item__c campingItem) { 9 | //insert the campingItem record 10 | insert campingItem; 11 | } 12 | 13 | /** 14 | * This method fetches the Camping_Item__c records and return as list 15 | */ 16 | @AuraEnabled 17 | public static List getItems() { 18 | //fetch the active records using soql query 19 | List campingItems = [SELECT Id,Name,Price__c,Packed__c,Quantity__c FROM Camping_Item__c]; 20 | //return the list of camping items 21 | return campingItems; 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /Save and Load Records with a Server-Side Controller/CampingListController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | clickCreateItem : function(component, event, helper) { 3 | var isFormValid = component.find("campingItemForm").reduce(function(isValid, inputCmp){ 4 | inputCmp.showHelpMessageIfInvalid(); 5 | return isValid && inputCmp.get("v.validity").valid; 6 | }); 7 | 8 | if (isFormValid) { 9 | 10 | var newCampingItem = component.get("v.newItem"); 11 | helper.createItem(component,newCampingItem); 12 | 13 | } 14 | }, 15 | 16 | doInit : function (component, event, helper) { 17 | var action = component.get("c.getItems"); 18 | action.setCallback(this,function (response) { 19 | var campingItems = response.getReturnValue(); 20 | component.set("v.items",campingItems); 21 | }); 22 | $A.enqueueAction(action); 23 | } 24 | }) -------------------------------------------------------------------------------- /Save and Load Records with a Server-Side Controller/README.md: -------------------------------------------------------------------------------- 1 | **Save and Load Records with a Server-Side Controller** 2 | 3 | Persist your records to the database using a server-side controller. The campingList component loads existing records when it starts up and saves records to the database when the form is submitted. 4 | 5 | * Create a CampingListController Apex class with a getItems method and saveItem method. 6 | * Add a doInit initialization handler that loads existing records from the database when the component starts up. 7 | * Modify the JavaScript controller to use a createItem method in the helper to save records to the database from a valid form submission. The new items are added to the controller's items value provider. 8 | -------------------------------------------------------------------------------- /Save and Load Records with a Server-Side Controller/campingList.cmp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 | 10 |
    11 | 13 | Add Camping Item 14 | 15 | 16 | 17 |
    18 | 22 | 23 | 29 | 30 | 36 | 37 | 40 | 41 | 45 | 46 | 47 | 48 |
    49 | 50 |
    51 | 52 |
    53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 | 67 |
    -------------------------------------------------------------------------------- /Save and Load Records with a Server-Side Controller/campingListHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | createItem : function(component,newCampingItem) { 3 | var action = component.get("c.saveItem"); 4 | action.setParams({ 5 | "campingItem" : newCampingItem 6 | }); 7 | 8 | action.setCallback(this,function(response){ 9 | var state = response.getState(); 10 | if (state === "SUCCESS") { 11 | var parsedCampingItem = JSON.parse(JSON.stringify(newCampingItem)); 12 | console.log(JSON.parse(JSON.stringify(parsedCampingItem)), JSON.stringify(parsedCampingItem)); 13 | var campingItems = JSON.parse(JSON.stringify(component.get("v.items"))); 14 | campingItems.push(parsedCampingItem); 15 | component.set("v.items",campingItems); 16 | component.set("v.newItem", {'Price__c': 0, 'Packed__c': false, 'Quantity__c': 0, 'Name':'', 'sobjectType': 'Camping_Item__c'}) 17 | } 18 | }); 19 | $A.enqueueAction(action); 20 | } 21 | }) -------------------------------------------------------------------------------- /Subscribe to a Platform Event in an Apex Trigger/OrderEventTrigger.apex: -------------------------------------------------------------------------------- 1 | trigger OrderEventTrigger on Order_Event__e (after insert) { 2 | // List to hold all tasks to be created. 3 | List tasks = new List(); 4 | 5 | // Get queue Id for task owner 6 | //Group queue = [SELECT Id FROM Group WHERE Name='Regional Dispatch' LIMIT 1]; 7 | String usr = UserInfo.getUserId(); 8 | // Iterate through each notification. 9 | for (Order_Event__e event : Trigger.New) { 10 | if (event.Has_Shipped__c == true) { 11 | // Create Task to dispatch new team. 12 | Task ts = new Task(); 13 | ts.Priority = 'Medium'; 14 | ts.Status = 'New'; 15 | ts.Subject = 'Follow up on shipped order ' + event.Order_Number__c; 16 | ts.OwnerId = usr;//queue.Id; 17 | tasks.add(ts); 18 | } 19 | } 20 | 21 | // Insert all tasks corresponding to events received. 22 | insert tasks; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Use Apex Metadata API to add a custom field to a page layout/UpdateContactPageLayout.apxc: -------------------------------------------------------------------------------- 1 | public class UpdateContactPageLayout { 2 | public Metadata.Layout addLayoutItem() { 3 | List layoutsList = Metadata.Operations.retrieve(Metadata.MetadataType.Layout, 4 | new List {'Contact-Contact Layout'}); 5 | Metadata.Layout layoutMetadata = (Metadata.Layout) layoutsList.get(0); 6 | Metadata.LayoutSection contactLayoutSection = null; 7 | 8 | for (Metadata.LayoutSection section: layoutMetadata.LayoutSections) { 9 | if (section.label == 'Additional Information') { 10 | contactLayoutSection = section; 11 | break; 12 | } 13 | } 14 | 15 | List contactColumns = contactLayoutSection.layoutColumns; 16 | List contactLayoutItems = contactColumns.get(0).layoutItems; 17 | 18 | Metadata.LayoutItem newField = new Metadata.LayoutItem(); 19 | newField.behavior = Metadata.UiBehavior.Edit; 20 | newField.field = 'AMAPI__Apex_MD_API_Twitter_name__c'; 21 | contactLayoutItems.add(newField); 22 | 23 | return layoutMetadata; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Use Apex Metadata API to add a custom field to a page layout/Use Apex Metadata API to add a custom metadata type to an org/MetadataExample.apxc: -------------------------------------------------------------------------------- 1 | public class MetadataExample { 2 | 3 | public void updateMetadata() { 4 | Metadata.CustomMetadata customMetadata = new Metadata.CustomMetadata(); 5 | customMetadata.fullName = 'MyNamespace__MyMetadataTypeName.MyMetadataRecordName'; 6 | 7 | Metadata.CustomMetadataValue customField = new Metadata.CustomMetadataValue(); 8 | customField.field = 'customField__c'; 9 | customField.value = 'New value'; 10 | 11 | customMetadata.values.add(customField); 12 | 13 | Metadata.DeployContainer deployContainer = new Metadata.DeployContainer(); 14 | deployContainer.addMetadata(customMetadata); 15 | 16 | Id asyncResultId = Metadata.Operations.enqueueDeployment(deployContainer, null); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Use a static resource to display an image on a Visualforce Page/ShowImage.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Use a static resource to display an image on a Visualforce Page/vfimagetest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataSolveProblems/Salesforce-Trailhead/f88c3a99f35161396e579c6566185c730f5ffc44/Use a static resource to display an image on a Visualforce Page/vfimagetest.zip -------------------------------------------------------------------------------- /Write an Apex trigger that modifies Account fields before inserting records/AccountTrigger.apxt: -------------------------------------------------------------------------------- 1 | trigger AccountTrigger on Account (before insert) { 2 | if(Trigger.isBefore && Trigger.isInsert) { 3 | AccountTriggerHandler.CreateAccounts(Trigger.new); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Write an Apex trigger that modifies Account fields before inserting records/AccountTriggerHandler.apxc: -------------------------------------------------------------------------------- 1 | public class AccountTriggerHandler { 2 | public static void CreateAccounts(List acctList) { 3 | for(Account a:acctList) { 4 | if(a.ShippingState != a.BillingState) { 5 | a.ShippingState = a.BillingState; 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Write an Apex trigger that modifies Account fields before inserting records/AccountTriggerTest.apxc: -------------------------------------------------------------------------------- 1 | @isTest 2 | public class AccountTriggerTest { 3 | @isTest static void TestCreate200Records() { 4 | List accts = new List(); 5 | for(Integer i=0; i<200; i++) { 6 | Account acct = new Account(Name='Test Account ' + i, BillingState='CA'); 7 | accts.add(acct); 8 | } 9 | 10 | Test.startTest(); 11 | insert accts; 12 | Test.stopTest(); 13 | 14 | List lstAccount = [SELECT ShippingState FROM Account]; 15 | for(Account a:lstAccount) { 16 | system.assertEquals('CA', a.ShippingState, 'ERROR'); 17 | } 18 | } 19 | } 20 | --------------------------------------------------------------------------------