Giter VIP home page Giter VIP logo

at4dx's Introduction

Advanced Techniques To Adopt SalesforceDX Unlocked Packages

This is an example set of code to demonstrate the techniques that @ImJohnMDaniel and @stohn777 used to adopt Salesforce DX Unlocked Packages.

The Wiki for this codebase goes over these concepts in depth.

Core Frameworks In This Project

Folder Description
sfdx-source/core Core library, contains the foundation code required to support all of the techniques

Sample Code for the frameworks can be found in the related project AT4DX Sample Code

Folder Description
sfdx-source/reference-implementation-common Demonstration code around the Accounts SObject including base domains and selectors. Also contains examples of Application Factory Injection
sfdx-source/reference-implementation-marketing Demonstration code around the addition of Marketing specific SObject fields on Account. Also contains examples of Application Factory Injection, Selector Field Injection, Domain Process Injection, and Test Data Builder Field Injection
sfdx-source/reference-implementation-sales Demonstration code around the addition of Sales specific logic. Also contains examples of Application Factory Injection, and Subscription Based Platform Events
sfdx-source/reference-implementation-service Demonstration code around the addition of Service specific logic. Also contains examples of Application Factory Injection, and Subscription Based Platform Events
sfdx-source/other Miscellaeous code. Used to increase the session settings for a scratch org to be 24 hours

Setup

This wiki article gives instructions on how to setup the codebase in a SalesforceDX Scratch org.

at4dx's People

Contributors

alanjaouen avatar bspeelm avatar chazwatkins avatar claychipps avatar daveespo avatar imjohnmdaniel avatar jstorey-milcorp-ita avatar madiraza avatar marcusyavorskyy-new10 avatar rygramer avatar stohn777 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

at4dx's Issues

Recent prioritized factory feature errors when Priority field is not specified

Existing CMDT records for Service or Selector are not supported, with the Priority field being required.

Steps to reproduce the behavior:

  1. Create a scratch org for the AT4DX sample code repo.
  2. Run the following Anonymous Apex....
  3. See error, "Required fields are missing: [Priority__c]"

Expected behavior
Previously sufficient CMDT records for Selectors and Services should be compatible.

image

Version
Per the current release as of 2023-08-14.

SelectorMethodInjectable return QueryLocator

Is your feature request related to a problem? Please describe.

When writting apex batch, it's a best practice to use Database.QueryLocator.
I tried to use method injection to inject my own logic in a shared selector like this

    public Database.QueryLocator start(Database.BatchableContext info) {
        return Application.Selector.newInstance(Account.SObjectType).selectInjection(AccountToProcessMethod.class, null);
    }

But selectInjection return a List<SObject>, as defined in ISelectorMethodInjectable.

Describe the solution you'd like

I would like be able to inject methods that return QueryLocator and not only List<SObject>.

Describe alternatives you've considered

I considered move my batch from Database.QueryLocator to Iterable<sObject>, but performance and limits with this are quite boring

SelectorMethodInjectable return List<object>

Is your feature request related to a problem? Please describe.
When writting selector with "group by" to SOQL, the query return a SObject "AggregateResult" type.
In test class it's not possible mock this selector because SObject "AggregateResult" it's readonly => SOQL Aggregate Functions

Ex :

public with sharing class MyCustomSelectorMethod extends AbstractSelectorMethodInjectable implements ISelectorMethodInjectable {
       public List<SObject> selectQuery() {

            MyCustomSelectorMethod.Parameters params = (MyCustomSelectorMethod.Parameters) getParameters();
            Set<Id> Ids = params.Ids;

             fflib_QueryFactory query = new fflib_QueryFactory(Contact.SObjectType)
                 .selectField(Contact.AccountId)
                 .selectField(Contact.CustomField)
                 .setCondition('AccountId IN :ids GROUP BY AccountId, CustomField);

            return Database.query(query.toSOQL());
       }
....
}

Describe the solution you'd like
I would like be able to inject methods that return List<object> and not only List<SObject>
Ex:

public with sharing class MyCustomSelectorMethod extends AbstractSelectorMethodInjectable implements ISelectorMethodInjectable {
       public List<object> selectQuery() 
       {
           ...
           List<SObject> result = Database.query(query.toSOQL());
           return this.convert(result);
       }
       private List<MyCustomResult> convert(List<SObject> objs)
       {
          -> convert AggregateResult[] To List<MyCustomResult>
          -> Return List<MyCustomResult>
       }
}

So I could transform in selector the SObjects aggregateResult to custom object class and return this custom object which are mockable

DomainProcessBinding__mdt.PreventRecursive__c is ignored.

Describe the bug
During trigger execution, criteria/action can be executed several time whatever the value set in DomainProcessBinding__mdt.PreventRecursive__c

As a result this can lead to additional limits consumption.

To Reproduce

I think I managed to reproduce the issue in my repo with a unit test:
master...simon-nowak:at4dx:preventRecursive

Expected behavior
I think any developper would like to be able to prevent recursive execution during a transaction. Projects like DLRS or flows can lead to recursive execution.

Version
Present in last version

I might have a few spare days this week and next week. If this is a feature that need to be implemented inside the framework, I would like to try to fix it.

I'm not 100% sure how this should work:

  • Can "prevent recursive" be applied for to criteria AND actions ?
  • If "prevent recursive" is applied what is the expected behaviour? Should it filter by "records" already evaluated, or be less precise and only focus on being called once?

AT4DX Questions

Hello @ImJohnMDaniel & @stohn777,

I am not sure if this is the appropriate communication channel to ask a few questions, but here I go.

  1. First question, to break business processes into Criteria's and Actions, does not only help organize the business logic into readable logical blocks but also creates a great layer within the metadata that allows our admins, architects, & business users get a better understanding of what is going on within the code. But from what I see with the approach taken in at4dx with criteria/action it is not friendly in terms of Execution Governors and Limits. For example, each criteria class might need to qualify records by soql'ing related data but also it might need to use this soql'ed data within the action class. With the current implementation of at4dx it will require you to re-soql the data which adds to the allowed transaction limits. Now if you have multiple processes for a particular SObject Domain that might add pretty quickly and Governors/Limits might become a problem. Have you considered this problem and how it might be solved? Potentially making the criteria class instances available to the actions. Or something more complex that allows bulkification of a transaction, by querying all the records required to complete a transaction within a domain for all criteria's & actions.

  2. Also the idea to execute bindings based on a DomainMethodToken it being either the TriggerOperation or set of specific process bindings to run for all trigger operations is great. I was wondering, if based on a particular SObject domain that has multiple processes defined by a RecordType and the requirement is to execute a set of bindings for a particular Trigger Operation and Record Type how would you accomplish this? I am assuming you are able to define your process bindings using the DomainMethodToken__c and then within each criteria class further qualify the records by the Trigger Operation. But this creates a gap when reviewing the Metadata Process Bindings records. I was just wondering if this is a valid scenario and if you have thought of making the Process Bindings be more specific for a process by using a combination of DomainMethodToken__c & TriggerOperation__c?

  3. As part of the at4dx model, I was wondering if it's wrong to think that the Action Class might need access to Existing Records as part of an update operation? and if passing the Existing Records to the Action Class is not advisable in this model.

Thanks in advance.

Extensible Selector

Reference

Use Case

In order to properly extend a class, the extension by default should fully represent the super, before any extending changes, per Liskov's Substitution Principle - the L in SOLID.

The paramount method to override is selectInjection, however enabling engineers to override other canonical functionality as needed is desirable, especially since virtualizing the other methods is backwards compatible and does not change functionality -- only freeing engineers to make changes if needed.

Proposed Solution

Virtualize all public or protected methods, enabling engineers to override specific methods as needed for overriding purposes.

image

Multiple, prioritized Selectors

Background

FFLIB and AT4DX offer many features that enable the development of applications using SOLID principles and facilitate very precises unit testing. However in non-productions environments, sometimes the application doesn't have data that would be available in Production and that data cannot be added to the SObject because of valid platform limitations. Therefore having the ability to override canonical logic with non-production substitutes is needed, providing development, testing, demonstrations, and other non-production activities a better representation of the intended business experience.

Reference

Solution Concept

Currently the binding logic prohibits multiple bindings for the same SObject.

  1. Remove the logic prohibiting bindings for the same SObject.
  2. Like the Domain-process-injection and Unit-of-Work bindings, provide a prioritization field. Null would correspond to the lowest priority.

In a non-production environment, engineers may deploy substitute selectors, having a higher priority, effectively overriding the canonical selector.

Illustration

Consider this use case where the application's canonical LoginHistory selector would not have any data supporting the business logic. With this enhancement in non-production environments, engineers could deploy a higher-priority Selector that acquires representative data from a different source, enable developers and non-production users the intended application experience.

image

Default behavior of including Standard Fields in ApplicationSObjectSelector tries to add inaccessible fields

This is more of a question than anything but what is the reasoning behind having getStandardFields()? I'm experiencing fflib_SecurityUtils exceptions due to some of the standard fields not being readable when it comes time to execute the query. It seems to me that it is making the assumption that users will have access to all standard fields for any object they have read access to, but I'm not sure if I'm missing something. At the very least, should it be its own thing separate from includeFieldSetFields since it's not even a field set?

If I do understand the problem correctly, a solution I'd like to propose is -if enforcingFLS- ignore fields that are inaccessible.

            Schema.DescribeFieldResult fieldDescribe = field.getDescribe();

            if ( ! fieldDescribe.isCustom() )
            {
                if ( fieldDescribe.isAccessible() || ! isEnforcingFLS() )
                {
                    standardFields.add( field );
                    system.debug( LoggingLevel.FINEST, 'Standard field : ' + field + ' added');
                }
                else
                {
                    system.debug( LoggingLevel.FINEST, 'Standard field : ' + field + ' NOT added');
                }
            }

Multiple, prioritized Service implementations

Background

FFLIB and AT4DX offer many features that enable the development of applications using SOLID principles and facilitate very precises unit testing. However in non-productions environments, many times the application doesn't have access to services and resources that will be available in Production. Therefore having the ability to override canonical logic with non-production substitutes is needed, providing development, testing, demonstrations, and other non-production activities a better representation of the intended business experience.

Solution Concept

Currently the binding logic prohibits binding for the same Interface to an implementation.

  1. Remove the logic prohibiting bindings for the same Interface.
  2. Like the Domain-process-injection and Unit-of-Work bindings, provide a prioritization field. Null would correspond to the lowest priority.

In a non-production environment, engineers may deploy substitute implementations, having a higher priority, effectively overriding the canonical implementations.

image

Illustration

Consider this use case where the application's canonical payment-processing logic calls a REST API to process the supplied payment credentials for a business transaction. During development, the canonical callout is not available and could be substituted with logic that returns responses and data per the card processors specifications. For testing or demonstrations, the card processor provides a testing-only instance to their service. In both cases, appropriate substitute logic or endpoints could be deployed that better support the application during that level of usage.

image

Event__e Platform Event API Name is too generic

Due to the naming of the Event__e Platform Event, installation of an at4dx unlocked package fails not only if there is a platform event with the same name, but also if there is a custom object Event__c.

Error message is:

(Event__e) That object name is already in use., Details: Event__e: That object name is already in use.

I know with the nature of no-namespace unlocked packages collisions are always a possibility, but I imagine there are plenty of other orgs with an Event__c. Could we rename to something more specific?

Unable to mock domain class

Hi John,

I'm very new to Apex Common and is very interested on trying it. I'm currently blocked when doing a unit test for a service that is calling a domain class. For my sample, I'm trying to convert the batch retry framework by afawcett.

The error I'm getting is: System.TypeException: Invalid conversion from runtime type IInvoices to fflib_SObjectDomain.IConstructable

Here are my code and also the unit test is below. Thank you in advance and really appreciate the help.

public interface IInvoices
    extends IApplicationSObjectDomain
{
    void executeInvoiceGenerationJob(List<Order> orders, fflib_ISObjectUnitOfWork uow);
}

public with sharing class Invoices
    extends ApplicationSObjectDomain
    implements IInvoices
{
    public static IInvoices newInstance(List<Invoice__c> records)
    {
        return (IInvoices) Application.Domain.newInstance(records);
    }

    public Invoices(List<Invoice__c> records)
    {
        super(records);

          // don't enforce CRUD security for Invoice records
        this.Configuration.disableTriggerCRUDSecurity();
    }

    public class Constructor
        implements fflib_SObjectDomain.IConstructable
    {
        public fflib_SObjectDomain construct(List<SObject> sObjectList)
        {
            return new Invoices(sObjectList);
        }
    }

    public void executeInvoiceGenerationJob(List<Order> orders, fflib_ISObjectUnitOfWork uow) 
    {
        List<Invoice__c> invoices = new List<Invoice__c>();
        for(Order order : orders) {
            produceSomeExceptions(order.Name);
            Invoice__c invoice = new Invoice__c();
            invoice.DueDate__c = System.today().addDays(30);
            invoice.Paid__c = false;
            invoice.Order__c = order.Id;
            invoice.Amount__c = order.TotalAmount;
            invoices.add(invoice);
            order.Invoiced__c = true;
            //register order for update 
            uow.registerDirty(order);    

            //register invoices for new
            uow.registerNew(invoice);        
        }
    }

    // Produce some exceptions, some catchable, some not...
    private void produceSomeExceptions(String orderName) {
        switch on orderName {
           when 'Ref:100' {
               // Standard exception
               throw new InvoiceGenerationException('Order Ref:100 is invalid.');
           }
           when 'Ref:300' {
               // Divide by zero!
               Integer newAmount = 20 / 0;
           }
        //    when 'Ref:500' {
        //         // Limits exception
        //         for(Integer idx=0; idx<201; idx++) {
        //             Order order = [select Id from Order limit 1];
        //         }
        //    }
        }
    }

    public class InvoiceGenerationException extends Exception {} 
}
public class InvoicesServiceImpl implements IInvoicesService {
    public void executeInvoiceGenerationJob(List<SObject> scope)
    {           
        // Create unit of work to capture work and commit it under one transaction
        fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();

        // Query Orders that are not yet invoiced          
        List<Order> orders = OrdersSelector.newInstance().selectByScopeAndIsInvoiced(scope, false);                    
        IInvoices invs = Invoices.newInstance(new List<Invoice__c>());
        invs.executeInvoiceGenerationJob(orders, uow);
        
        uow.commitWork();
    }

    public List<Order> getNotInvoicedOrdersThisWeek()
    {
        // Query Orders that are not yet invoiced
        return OrdersSelector.newInstance().selectByEffectiveDateAndIsInvoiced(false);
    }

    public void handleGenerationJobErrors(brf_BatchableError error)
    {
        // Update the effected orders with the error for further review

        fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
        IOrders orders = Orders.newInstance(new List<Order>());
        orders.updateOrdersByErrorId(error, uow);     
    }
}
@IsTest
private class InvoicesServiceImplTest {
    @IsTest
    private static void callingServiceExecuteInvoiceGenerationJob_ShouldCallOrdersSelectorAndDomain()
    {
        // Create mocks
        fflib_ApexMocks mocks = new fflib_ApexMocks();
        IApplicationSObjectUnitOfWork uowMock = (IApplicationSObjectUnitOfWork) mocks.mock( IApplicationSObjectUnitOfWork.class );               
        IInvoices domainMock = (IInvoices) mocks.mock( IInvoices.class );       
        IOrdersSelector selectorMock =  (IOrdersSelector) mocks.mock( IOrdersSelector.class );

        // Given
        mocks.startStubbing();
        Order ord = new Order(
            Id = fflib_IDGenerator.generate(Order.SObjectType),
            Name = 'Ref:0',
            Invoiced__c = false,
            Status = 'Draft');

        List<string> orderIds = new List<string>();
        orderIds.add(ord.Id);        
        List<Order> testOrders = new List<Order>();
        testOrders.add(ord);

        SObject obj = new Order();
        obj.Id = ord.Id;
        List<SObject> scope = new List<SObject> { obj };
        mocks.when(domainMock.sObjectType()).thenReturn(Invoice__c.SObjectType);             
        mocks.when(selectorMock.sObjectType()).thenReturn(Order.SObjectType);
        mocks.when(selectorMock.selectByScopeAndIsInvoiced(scope, false)).thenReturn(testOrders);

        mocks.stopStubbing();
        //set mocks
        Application.UnitOfWork.setMock(uowMock);
        Application.Selector.setMock(selectorMock);
        Application.Domain.setMock(domainMock);

        //When
        InvoicesService.executeInvoiceGenerationJob(scope);

        // Then
        ((IOrdersSelector) 
            mocks.verify(selectorMock)).selectByScopeAndIsInvoiced(scope, false);
        ((IInvoices) 
            mocks.verify(domainMock)).executeInvoiceGenerationJob(testOrders, uowMock);
        ((fflib_ISObjectUnitOfWork) 
            mocks.verify(uowMock, 1)).commitWork();
    }
}

Unable to call TestDataSupplementer.supplement(xxx) if no implementation of TestDataSupplementer for SObjectType xxx

Hello here,

First I'm not sure if it's a bug or a wanted behaviour.

Is your feature request related to a problem? Please describe.
My context: package B depends on package A.
image

If
package A own SObject Account and have tests that insert Account records. Package A doesn't contain any TestDataSupplementer on SObject Account.
And
package B implement a VR and mandatory custom field on Account. Package B implement a TestDataSupplementer for that.

Then in a environment where only package A is installed the line

TestDataSupplementer.supplement(accounts); 

will fail with error

system.NullPointerException: Attempt to de-reference a null object  
Class.TestDataSupplementer.supplement: line 59, column 1     

Describe the solution you'd like
Trying to supplement a SObject that has no implemented TestDataSupplementer should not fail.

Describe alternatives you've considered
For now we are implementing empty TestDataSupplementer on low level package in hierarchy to avoid this problem.

Additional context
All our tests in package are not 100% mocked. We mostly mock them but not always. This allow us to be able to test governor limits if needed.

Our CI is running unit test on the current package AND all unit tests of all package in a sandbox. We have a some tests that fail on the sandbox that we cannot be fixed without supplementing them.

Our goal is to be able to always run all tests of all package inside a sandbox to have a more reliable delivery process (fail early in the development process).

When a package implement an integration test (with real insert) we would like to be able to supplement it regardless the hierarchy level.

Selector layer questions

Hi @ImJohnMDaniel @stohn777 ,

I would like to ask about 2 functionalities that I tried to use, but I couldn't find the built-in solution:

  1. Aggregate Queries - is there any way to do that within the selector layer without direct/dynamic queries? I found the information that it's one of the limitations of the AT4DX, but I'd like to know whether there was any specific reason it's not implemented.
  2. Polymorphic fields TYPEOF clause - it's available since API 46.0, but I couldn't find any built-in solution to use it with query factory. Are there any plans to implement it?

I really appreciate all the work you've been doing with AT4DX!

Best regards,
Matt

Testing Application Factory without existing CMDT config data

John,

In working on creating unit tests for the Application Factory, my team stumbled upon a difficulty in trying to test it. As far as I can tell, there is no easy way to give it "mock" CMDT records with which to work, in writing unit tests for the application factory code itself.

I handled the same issue when I wrote my own SObject type to SObject type binding capability that I have described to you previously. In that instance I had to make my own binding provider. When I did that, I had it pull the important mapping data from the CMDT into an @testvisible private static map property.

    @testVisible
    private static Map<String,Map<SObjectType,SObjectType>> SObjectTypeBindingMap{
        get{
            if(SObjectTypeBindingMap == null){
                SObjectTypeBindingMap = new Map<String,Map<SObjectType,SObjectType>>();
                List<base_SObjectTypeBinding__mdt> sotBindings = [SELECT Id
                                                                    , DeveloperName
                                                                    , Purpose__c
                                                                    , BindingSObjectType__c
                                                                    , BindingSObjectType__r.QualifiedApiName
                                                                    , BoundSObjectType__c
                                                                    , BoundSObjectType__r.QualifiedApiName
                                                                    FROM base_SObjectTypeBinding__mdt];
                for(base_SObjectTypeBinding__mdt sotBinding : sotBindings){
                    if(!SObjectTypeBindingMap.containsKey(sotBinding.Purpose__c)){
                        SObjectTypeBindingMap.put(sotBinding.Purpose__c,new Map<SObjectType,SObjectType>());
                    }
                    SObjectTypeBindingMap.get(sotBinding.Purpose__c).put(sObjectTypesMap.get(sotBinding.BindingSObjectType__r.QualifiedApiName),sObjectTypesMap.get(sotBinding.BoundSObjectType__r.QualifiedApiName));
                }
            }
            return SObjectTypeBindingMap;
        }
        set;
    }

This way I could easily "mock" the configuration data for unit tests, by just setting that property.

I was thinking of modifying your other application factory classes to do something similar. Before I did that, I thought it would be a prudent to ask how you have dealt with this? I want to make sure I'm not just missing something obvious.

Thanks, as always, for your time and attention.

AT4DX Documentation

Hi @ImJohnMDaniel - As requested in PR #70, I moved the docs to the wiki in my fork. I will start contributing the docs there, and we can merge them into the main at4dx repo when ready.

Fork Wiki: https://github.com/chazwatkins/at4dx/wiki

Changes:
I started writing a draft documentation on the Domain Process feature. Check it out in Domain Process Injection and let me know your feedback @ImJohnMDaniel @stohn777

So far, it's mostly a brain dump and includes some explanation provided in issues. So, I'm sure it'll need some reordering and wordsmithing.

Theory behind setMock in the Application factories

When you call the setMock method in the Application factories, it turns around and instantiates a new force-di injector, which ultimately calls replaceBindingsWith. This replaces the binding in the current list of bindings with the mock instance. But if there is no binding configured (ie there’s no custom metadata record), the mock isn’t set.

I would assume that if I call setMock, the current binding would be replaced if there is a binding configured OR if a binding doesn’t exist, the mock would be set as the binding. Basically, no matter what, a call to setMock results in the mock that is passed in being set as the binding used by force-di.

What am I missing?

Add pseudonamespace to project

Annoying to keep up to date with upstream changes when we have to manually pseudonamespace the project.

I know this project started of as an example but there's no reason not to add a pseudonamespace.

Unique constraint on CMDT BindingSObject__c fields is restrictive.

By the way John, I am for my own purposes removing the "unique" tag from the BindingSObject field on the CMDTs you've provided. My little pet problem I'm trying to solve as you know, is one of dynamically handling SObjects. Therefore, it is helpful for me if I can map a single selector or domain class, to a variety of SObjects. I don't suppose that is a normal thing to do, and so probably not pertinent to anyone else, but I thought you might like to know that for me at least, it's better without that.

calling TestDataSupplement

Does the TestDataSupplementer.supplement method need to be called explicitly in each test method?
I don't see where it is called anywhere in the framework.

So using the at4dx-sample example. Where does the upstream package call the TestDataSupplementer.supplement so that the MarketingFieldsForAccountsSupplementer can insert the "Hellow World!" slogan.

My exact issue is that we have a required field on Product2 and now the fflib tests are failing:
fflib_SObjectUnitOfWorkTest.testDerivedUnitOfWork_CommitSuccess Fail System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Part_Number__c]: [Part_Number__c] 122

How to handle platform objects in selectors? (groups, users,...)

Hello,

I'm trying to convert my application that is using the fflib to the AT4DX framework (we are moving our architecture to unlocked packages). In the past we used the selectors to query data from:

  • groups
  • users
  • permission sets...

And I just can't use those objects in the ApplicationFactory_SelectorBinding. I was thinking to put them in our "core" package.

How this should be handle?

When should ApplicationFactory_UnitOfWorkBinding and DomainMethodToken be used?

Hi @ImJohnMDaniel @stohn777 ,

Can you provide some examples for using ApplicationFactory_UnitOfWorkBinding and Domain Process Action DomainMethodToken?

I don't understand when I'd reach for either option. I checked for these in at4dx-samplecode, but didn't see any examples.

Also, I found @ImJohnMDaniel response to an earlier question in #39 about DomainMethodToken, but would like to see an example for understanding.

re: DomainMethodToken vs TriggerOperation -- So the thought here is that it is an "either/or" feature. You will specify the TriggerOperation approach or the DomainMethodToken approach. The DomainMethodToken option is for use cases where you are wanting to call an orchestrated domain process from a Service tier class or maybe another Domain class. In theory could call another Domain that is part of another package and simply wrap the call in a try/catch thus allowing for the functionality to be called, if the code were present in the org, and it still be loosely coupled.

Thanks for your work on this great framework! My team and I have recently picked up AT4DX to aid us in breaking up our organization into packages. It has been a joy to use so far.

Best Regards,
Chaz Watkins

Construction of Unit of Work Slow

We are trying to create an unit of work using the AT4DX framework with caching enabled and we noticed that it takes about 2.5 seconds to generate a new UoW.

Org details:

  • Number of sObjects: 198
  • Number of bindings for Unit of work: 12
  • Steps to reproduce:
System.debug('Start : ' + Limits.getCpuTime());
IApplicationSObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
System.debug('Stop : ' + Limits.getCpuTime());

How to get a selector without sharing

What is the best way to have one selector implementation and be able to choose to have a with sharing and a without sharing selector, with the minimal amount of code duplication.

I currently have something like this, but how do I implement this in AT4DX:

public interface IContactsSelector extend fflib_ISObjectSelector
{
    List<Contact> selectById(Set<Id> ids);
 }
pubic virtual inherited sharing class ContactsSelector extends fflib_SObjectSelector implements IContactsSelector
{
   public static IContactsSelector newInstance()
   {
       return (IContactsSelector) Application.Selector.newInstance(Schema.Contact.SObjectType);
   }
   public static IContactsSelector newWithoutSharingInstance()
   {
       return (IContactsSelector) Application.WithoutSharingSelector.newInstance(Schema.Contact.SObjectType);
   }
   public static IContactsSelector newWithSharingInstance()
   {
       return (IContactsSelector) Application.WithSharingSelector.newInstance(Schema.Contact.SObjectType);
   }
   public ContactsSelector()
   {
       super();
   }
   public ContactsSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS)
   {
       super(includeFieldSetFields, enforceCRUD, enforceFLS);
   }
   public List<Contact> selectById(Set<Id> ids);
   {
      ...
   }
   
   public with sharing class WithSharing extends ContactsSelector
   {
       public WithSharing()
       {
           super(true, true, true);
       }
       public override List<Contact> selectById(Set<Id> idSet)
       {
           return super.selectById(idSet);
       }
   }
   public with sharing class WithoutSharing extends ContactsSelector
   {
       public WithSharing()
       {
           super(true, false, false);
       }
       public override List<Contact> selectById(Set<Id> idSet)
       {
           return super.selectById(idSet);
       }
   }
}

Binding__mdt.BindingObject__c data type does not support all SObject types

This change is related to Force-DI project, Issue #33.

It was discovered that the MetadataRelationship to EntityDefinition data type for the the various Binding metadata types BindingObject__c field does not support all SObject types available. Most notably is that it does not support the User SObject, but it also does not support several other SObjects including BusinessHours, Event, FiscalYearSettings, Group, Holiday, and several others.

This was confirmed as a limitation with the MetadataRelationship EntityDefinition data type by the Salesforce Custom Metadata Type product manager. It is not a limitation with this codebase nor the Force-DI codebase.

The solution on the Force-DI project was to add an additional field to be used to specify the SObjectTypes that are not available in the EntityDefinition MetadataRelationship.

This issue cover the similar changes needed to this codebase

TestDataSupplementer misuses BindingSObject__c for custom SObjects

TestDataSupplementer acts like BindingSObject__c will always contain the developer name of the SObject, but that's not the case.

SObjectType sobjType = Schema.getGlobalDescribe().get( String.isNotBlank(tds_mdt.BindingSObject__c) ? tds_mdt.BindingSObject__c : tds_mdt.BindingSObjectAlternate__c );

For standard objects such as Account or Contact, BindingSObject__c will just be "Account" or "Contact" respectively, which is no problem.
But if the BindingSObject__c is not standard, that field value contains an ID for the Entity Definition like "01I1k00000143aP", and because the global schema describe is keyed by DeveloperName, sobjType will always be null.

So what we are really looking for is bindingConfig.BindingSObject__r.QualifiedApiName but this is not accessible in TestDataSupplementer.cls due to using di_Injector.Org.Bindings.byName(ITestDataSupplement.class.getName()).get() to retrieve the MDT.

That's the extent of the problem as I see it. The workaround is obviously to use BindingSObjectAlternate__c. I don't know another solution up front, unless there's a way to get an SObjectType by its 01I ID.

Selector Layer / question about injecting method or criterias

Hi @ImJohnMDaniel @stohn777 ,

I'm starting to implement AT4DX in my company, and this framework help us a lot for switching to unlocked package, so first of all thank you for sharing this with us.

I'm wondering about a use case for splitting my monolithic code in package:

Let's says that:

  • PackageA define AccountSelector
  • PackageB define a field packageB_id__c on Account

I would like to define in packageB a methode like AccountSelector.getAccountByPackageB_id__c(Set<String>)

I did not find any exemple about this use case, should I create an AccountSelector specifically for PackageB?

Thanks for your time,

Best
Alan

System.TypeException: Cannot call test methods in non-test context

I think something is a bit off about the TestDataSupplementDIModule code.
I get the following error the first time I access bindings of any kind while that module's binding record exists.

18:46:47:037 EXCEPTION_THROWN [EXTERNAL]|System.TypeException: Cannot call test methods in non-test context
18:46:47:037 METHOD_EXIT [263]|01p19000000uaTh|TestDataSupplementDIModule.configure()
18:46:47:037 METHOD_EXIT [279]|01p19000000uZ3i|di_Binding.Resolver.loadBindings()
18:46:47:037 METHOD_EXIT [87]|01p19000000uZ3i|di_Binding.Resolver.get()
18:46:47:037 METHOD_EXIT [60]|01p19000000uZ3n|di_Injector.getInstance(String, Object)
18:46:47:037 METHOD_EXIT [123]|01p19000000uZ3n|di_Injector.getInstance(System.Type)
18:46:47:037 METHOD_EXIT [3]|01p19000000uZ4v|Application.ServiceFactory.newInstance(System.Type)
18:46:47:037 METHOD_EXIT [18]|01p19000000uZ5z|tagr_TagsService.service()
18:46:47:037 METHOD_EXIT [43]|01p19000000uZ5z|tagr_TagsService.getTagsByRelatedSObject(Set)
18:46:47:037 FATAL_ERROR System.TypeException: Cannot call test methods in non-test context
18:46:46:000 FATAL_ERROR Class.di_Binding.Resolver.loadBindings: line 263, column 1
18:46:46:000 FATAL_ERROR Class.di_Binding.Resolver.get: line 279, column 1
18:46:46:000 FATAL_ERROR Class.di_Injector.getInstance: line 87, column 1
18:46:46:000 FATAL_ERROR Class.di_Injector.getInstance: line 60, column 1
18:46:46:000 FATAL_ERROR Class.Application.ServiceFactory.newInstance: line 123, column 1
18:46:46:000 FATAL_ERROR Class.tagr_TagsService.service: line 3, column 1
18:46:46:000 FATAL_ERROR Class.tagr_TagsService.getTagsByRelatedSObject: line 18, column 1
18:46:46:000 FATAL_ERROR Class.tagr_FrameController.getRelatedTags: line 43, column 1
18:46:47:037 FATAL_ERROR System.TypeException: Cannot call test methods in non-test context

I have simply eliminated the TestDataSupplement binding record and the error went away. So, no problem for me really, unless I want to try that functionality out, but I thought you would want to know.

Extending PlatformEventDistributor

We have need, in a very large scale org, to extend the PlatformEventDistributor to allow for synchronous execution of subscribed classes, rather than using a Queueable. We've discussed a few options around this, and based upon the results of this ticket I will be submitting a pull request in the next few days to cover this enhancement.

What it basically comes down to is this:

  • Adding a checkbox to the PlatformEvents_Subscriber__mdt to allow indicating a subscription as synchronous ("Call Synchronous" or "Run In Process", defaulting to false/unchecked, not required).
  • Checking that value in the triggerHandler portion of PlatformEventDistributor to either call enqueueJob(consumer) or consumer.runInProcess() appropriately.

The trick, and therefor the purpose of this "issue", is to determine how to implement this through the Interface, IEventConsumer.

Option 1: simply add IEventConsumer.runInProcess().
Impact: This will affect any org that has adopted this package and made use of this process thus far. All implementations of IEventConsumer would fail, and runInProcess() would have to be provided a body, even if it did nothing, before use of this pattern could continue in those orgs.
On the bright side, this would allow properly implemented consumers to toggle between async and sync via the new checkbox with no code changes in the Consumer.

Option 2: add a new interface for sync consumers, IE: IEventSyncConsumer or IEventInProcessConsumer
Impact: A lot more code changes to PlatformEventDistributor, and a code change would be required when switching configured consumers between sync and async mode. There would be no ability to quickly toggle between modes.

I am happy to deliver option 2 in order to ease any pain on previous adopters. However, the cleaner and more elegant solution is certain option 1. I am also open to other options for implementation, including keeping said changes within my own org and not pushing them to this repo (if that is desired).

Custom Settings to Disable Triggers (via hier custom settings)

Need the ability to disable triggers via Hier. Custom Setting. I am planning on creating a sub-dir framework-application-configuration to contain IApplicationConfiguration that allows one to get the IDomainConfiguration, ISelectorConfiguraiton and IServiceConfiguration. Why 3? Interface Segregation. Will start w/ IDomainConfiguration would initially define the following:

 // is the sobject triggers enabled (i.e. 'Account', 'Contact', or 'All' ...) [_default impl, returns true_]
 boolean isTriggerEnabled(string sobjectName);
 // trace/debug information wanted ... instead of 'system.debug'; use a configuration to turn on/off
 // this cross-cuts other configurations but here for completeness [_default impl returns false_]
 boolean isTracing();

So why this way instead of active/de-active in the CMT ? Bec/ different users/profiles will have different needs. For example, a system integrator performing bulk loads may run under a profile that wants triggers disabled. For others, the triggers are enabled.

If you have any comments/issues please let me know ... WIP.

Bindings to custom SObjects fail.

I'm just trying to get started using this to implement fflib-apex-common, so that I can support packaging well. I may have missed something about how this is meant to be used, because there isn't much documentation yet. I think I have it fairly well understood though.

When I map Account to a selector it works perfectly. But if I map a custom SObject to a selector, it throws...

11:01:02:457 FATAL_ERROR System.InvalidParameterValueException: Invalid sobject provided. The Schema.describeSObject() methods does not support the 01i0r000000a9mu sobject as a parameter. The sobject provided does not exist.
11:01:02:000 FATAL_ERROR Class.ApplicationSObjectSelectorDIModule.configure: line 18, column 1
11:01:02:000 FATAL_ERROR Class.di_Binding.Resolver.loadBindings: line 263, column 1
11:01:02:000 FATAL_ERROR Class.di_Binding.Resolver.get: line 279, column 1
11:01:02:000 FATAL_ERROR Class.ApplicationSObjectSelector.incorporateAdditionalSObjectFields: line 33, column 1
11:01:02:000 FATAL_ERROR Class.ApplicationSObjectSelector.: line 50, column 1
11:01:02:000 FATAL_ERROR Class.tagr_RootsSelector.: line 9, column 1
11:01:02:000 FATAL_ERROR Class.tagr_RootsSelector.: line 6, column 1
11:01:02:000 FATAL_ERROR Class.tagr_RootsSelectorTest.getSObjectTypeTest: line 8, column 1

It's because the following line is expecting a name but getting an Id from the CMDT.
results = Schema.describeSObjects(new String[] { bindingConfig.BindingSObject__c.toLowerCase().trim() });

Here is my interface...

public interface tagr_IRootsSelector extends IApplicationSObjectSelector{
}

I just created a simple implementation of ApplicationSObjectSelector...

public class tagr_RootsSelector extends ApplicationSObjectSelector implements tagr_IRootsSelector{
    public static tagr_IRootsSelector newInstance(){
        return (tagr_IRootsSelector)Application.Selector.newInstance(tagr_Root__c.SObjectType);
    }
    public tagr_RootsSelector(){
        this(true,true,true);
    }
    public tagr_RootsSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS){
        super(includeFieldSetFields, enforceCRUD, enforceFLS);
    }
    public Schema.SObjectType getSObjectType(){
        return tagr_Root__c.sObjectType;
    }
    private List<Schema.SObjectField> getAdditionalSObjectFieldList(){
        return new List<Schema.SObjectField>{};
    }
// I Have added some different selector methods here. I have removed them for brevity as this bug happens when the class is constructed.
}

Then I just started to create a test class so that I could see it in action and make sure that I was understanding it...

@isTest
public class tagr_RootsSelectorTest{
    @testSetup
    private static void setup(){
    }
    @isTest
    private static void getSObjectTypeTest(){
        tagr_RootsSelector testSelector = new tagr_RootsSelector();
        System.assertEquals(tagr_Root__c.sObjectType, testSelector.getSObjectType());
    }
}

Run that test and you get the previously mentioned error.

It is simply because the CMDT returns an Id for custom SObjects rather than a name. Since describeSObjects expects the name, it fails. As I said previously, there doesn't appear to be a super easy way to convert from an Id to a name. Maybe I just don't know it though. From what I can tell, the only way is to do a global describe and then iterate all the types till you find the one with the Id you're looking for. I had thought of simply mapping all of that out in some central place so that it could be accessed by any of the classes.

Also, I actually originally saw the issue in a different place because I had a regular "di_Binding__mdt" that referenced a custom SObject in the "BindingObject__c" field. So, it is also a problem in that way.

Class.di_Injector.CustomMetadataModule.configure: line 147, column 1
Class.di_Binding.Resolver.loadBindings: line 259, column 1
Class.di_Binding.Resolver.get: line 279, column 1
Class.ApplicationSObjectSelector.incorporateAdditionalSObjectFields: line 33, column 1
Class.ApplicationSObjectSelector.: line 50, column 1
Class.tagr_RootsSelector.: line 9, column 1
Class.tagr_RootsSelector.: line 6, column 1
Class.tagr_RootsSelectorTest.getSObjectTypeTest: line 8, column 1

Essentially, if you're going to pull the BindingObject out of a CMDT that uses an entity field, you will need to translate any custom SObjects from Ids to Names or else use a different method of getting the describe info than Schema.describeSObjects. Unless you know a better way, I suppose the easiest solution is just to do a global describe from the beginning, mapping names to Types and use it to get the answer, rather than ever using Schema.describeSObjects.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.