apex-enterprise-patterns / force-di Goto Github PK
View Code? Open in Web Editor NEWGeneric DI library with support for Apex, Triggers, Visualforce and Lightning
License: BSD 3-Clause "New" or "Revised" License
Generic DI library with support for Apex, Triggers, Visualforce and Lightning
License: BSD 3-Clause "New" or "Revised" License
For simple use cases with Lightning pages where the developer just wants a Lightning component on the page that would only be using the <c:di_injector>
tag, then to reduce the amount of boiler plate code developers need to bring to the table we can provide one for them.
This new component would expose design attributes via datasource so the admin can select from the list of CMDT bindings available.
This new component is a simple wrapper for <c:di_injector>
tag for use with drag-n-drop in App Builder when creating Lightning pages.
For passing in custom attributes (<c:di_injectorAttribute>
), need to continue to think about the design:
I already see there is a PR created for increasing the code coverage. Is there any ETA?
PR link: #63
Hi Andy
Great stuff.
One feature that might be of use instead of using custom metadata you could you SOSL to find your classes that extend di_module.
List<List<SObject>> results = [FIND ' extends di_Module' IN ALL FIELDS RETURNING ApexClass (Id, Name)];
for(SObject result : results.get(0)){
System.debug(result.get('Name'));
}
It would be a relatively quick query and you could also cache the data in the platform cache.
I was contemplating using this to create my own annotations framework for DI but I thought it might be too temperamental and require too much processing time for the initial setup/config.
Although I'm not sure how this would work with unlocked packages if the code would be searchable or not.
As recently pointed out in another related blog (cannot find it now?)... JSON deserialization has some benefits.
Small housekeeping issue.
The unit tests should be moved from force-di/main/default/classes/test over to /force-di/test/classes. Tests should be in their own directory away from the "main" code.
Add functionality to manually clear out all Bindings from Platform Cache from execute anonymous or Apex class
An exception is thrown when the di_Binding tries to load a Binding__mdt record that has the Binding Object (Binding__mdt.BindingObject__c) field mapped to a custom SObject (i.e. Widget__c).
RuntimeBindingDemoOrg.run();
ERROR: Execution failed.
▸ ERROR: System.InvalidParameterValueException: Invalid sobject provided.
▸ The Schema.describeSObject() methods does not support the 01i3b00000034c4
▸ sobject as a parameter. The sobject provided does not exist.
▸ ERROR: Class.di_Injector.CustomMetadataModule.configure: line 147, column
▸ 1
▸ Class.di_Binding.Resolver.loadBindings: line 265, column 1
▸ Class.di_Binding.Resolver.get: line 277, column 1
▸ Class.di_Injector.getInstance: line 87, column 1
▸ Class.di_Injector.getInstance: line 60, column 1
▸ Class.RuntimeBindingDemoOrg.WelcomeApp.<init>: line 16, column 1
▸ Class.RuntimeBindingDemoOrg.run: line 30, column 1
The issue appears to be that when CMDT records with fields of the data type "Metadata Relationship(Entity Definition)", the value returned on that field in Apex will be the API name of the SObject if that SObject is a Standard SObject. If it contains the value of a Custom SObject, then the internal SObject Record ID is returned instead. That is not accepted on the Schema.describeSObject()
method call in the di_Injector.CustomMetadataModule.configure()
method call on line 147.
Describe the bug
I am seeing an error when the binding value is modified in the parent component's init handler.
To Reproduce
Testapp.app
<aura:application extends="force:slds">
<aura:attribute name="buttonLabel" type="String" default="Neutral"/>
<aura:handler name="init" value="{!this}" action="{!c.initHandler}"/>
<!-- create following button component using Force-DI -->
<!-- <lightning:button label="Neutral"></lightning:button> -->
<c:di_injector bindingName="lc_button">
<c:di_injectorAttribute name="label" value="{!v.buttonLabel}"/>
</c:di_injector>
</aura:application>
TestappController.js
({
initHandler : function(cmp, evt, helper) {
cmp.set('v.buttonLabel', 'brand');
}
})
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<label>lc_button Binding</label>
<protected>false</protected>
<values>
<field>BindingObjectAlternate__c</field>
<value xsi:nil="true"/>
</values>
<values>
<field>BindingObject__c</field>
<value xsi:nil="true"/>
</values>
<values>
<field>BindingSequence__c</field>
<value xsi:nil="true"/>
</values>
<values>
<field>To__c</field>
<value xsi:type="xsd:string">lightning:button</value>
</values>
<values>
<field>Type__c</field>
<value xsi:type="xsd:string">LightningComponent</value>
</values>
</CustomMetadata>
Steps to reproduce the behavior:
Expected behavior
No error expected.
Screenshots and text of error observed
Version
Did you try to reproduce the problem against the latest force-di code? yes
Improve the UI so that it creates a record and thus avoid having to programmatically create a record to experience the rest of the demo.
I was wondering if there is also support for sub-classes in force-di, like in the example below:
public static IContactsSelector newWithoutSharingInstance()
{
return (IContactsSelector) di_Injector.Org.getInstance(ContactsSelector.WithoutSharing.class));
}
I am able to execute the code but I am not able to create a Custom Metadata record for this in di_Binding__mdt, as a "." is not allowed in the Binding Name.
Or is there another way of doing this?
The main use case is that I have a (sub)package within the application that requires a selector in an elevated context. To avoid code duplication we added a sub class in the main selector class..
public virtual inherited sharing class ContactsSelector extends fflib_SObjectSelector implements IContactsSelector
{
public ContactsSelector()
{
super();
}
public ContactsSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS)
{
super(includeFieldSetFields, enforceCRUD, enforceFLS);
}
...
public without sharing class WithoutSharing extends ContactsSelector
{
public WithoutSharing()
{
super( true, false, false );
}
public override List<Contact> selectById(Set<Id> idSet)
{
return super.selectById(idSet);
}
...
}
}
A Null Pointer exception has been observed in certain conditions. Specifically, the di_Injector
's method getInstance(String, Schema.SObjectType, Object)
could through an NPE when the bindingsFound
returns a null entry.
List<di_Binding> bindingsFound = this.Bindings.bySObject( bindingSObjectType )
.byName( developerName.toLowerCase().trim() )
.get();
if ( bindingsFound == null || bindingsFound.isEmpty() ) {
throw new InjectorException('Binding for "' + developerName + '" and SObjectType "' + bindingSObjectType + '" not found');
}
return bindingsFound[0].getInstance(params);
If bindingsFound
contains a null in the first position of the list, the di_Bindings.getInstance(Object)
call will fail.
In force-di
directory, add list of binding parameters to the Binding.cls
class so that default parameters can be provided at config time.
Adding parameters at runtime via the current getInstance(params)
method would merge the runtime parameters with the default parameters.
Binding parameters would have four properties:
Both the Binding.cls for Apex API and the Binding__mdt needs to support the related binding parameters. For metadata configuration, propose a new Binding_Param__mdt whose parent is a Binding__mdt.
When running the unit tests, I'm getting a failure for: di_BingingParamTest.givenStringsWhenGetParameterThenGetValues(), line 302...
System.assertEquals( testDateTime.dateGmt(), response.dateTimeValue ); // hours will be zero'ed out
I'm in NZ Summer Time time zone. The expected value: 2018-08-07, actual value: 2018-08-08
Running this at 8:00 am in NZ
di_PlatformCache will silently fail under the following conditions:
di_Injector.Org.Bindings.byName(String)
.bySObject(SObjectType)
.emptyBindingsAllowed()
.get();
Under these conditions, the call to di_Binding.get()
method first tries to retrieve the bindings from the platform cache. If that returns no bindings and empty bindings are allowed, then the call to loadBindings()
method is bypassed.
The root issue is in the method di_PlatformCache.isStoringBindingInPlatformCache()
. That method currently only checks value in the di_Configurations__c.UsePlatformCacheToStoreBindings__c field and does not also consider that the partition specified in di_Configurations__c.OrgCachePartitionName__c may not be valid.
The fix should be to change the di_PlatformCache.isStoringBindingInPlatformCache()
method to also consider if the partition specified is valid.
Hello gentlemen. First of all, thanks for all your hard work. This is awesome.
I have a DI use case where I would like to inject an sObject based on the type of another sObject. So, for example, I have a lightning component that "hasRecordId" and it would like determine the sObject type of that record Id and pass that information along and get another related sObject in return, without having to have any knowledge of it, except that it can expect to get back an sObject that meets certain requirements. The idea here is to make the lightning component be versatile and capable of working with new sObjects based purely on the addition of an appropriate sObject and a CMDT entry that links them.
Essentially this is sObject dependency injection. So, I can for example, have the package work with Accounts and have a related object for it that has the fields etc my component is expecting to work with. Then, if I decide to install CPQ, now maybe I need a new similar sObject that does the same thing, but is linked to SBQQ__Quote__c instead of Account. In my case I am using these sObjects as many to many linking objects between an sObject in the package and other sObjects. I would like it to be dynamic and be able to add support for additional sObjects based simply upon adding a new linking object and a CMDT entry. This lets me still have working Salesforce reporting and I can work with any sObject without having to make any changes to my package and it's classes and components.
I know I can do this by just adding my own CMDT, but I am already using Force DI and it seems like a natural extension of Force DI. So, I was thinking I could simply add another field to the Force DI CMDT such as "Bound Object", and add some appropriate methods to Force DI. That way if I'm trying to bind an sObject, it's just a matter of filling out that field. You would simply leave it empty just as you do the "Binding Object" field, if you didn't need it. This way sObjects could be bound dynamically in all the same ways that Classes are. You don't have interfaces for sObjects, so the user is responsible for making sure the sObject will fit the requirements or it will cause errors, but that is unavoidable.
So, my question is, do you see any reason that this is not a good fit or that I should just make a different CMDT and not involve Force DI?
Thanks for your time,
Tory Netherton
Report from Matt Comer.
The
di_PlatformCache.constructKeyName()
method sometimes produces negative values from the.hashcode()
function. It also does not filter out double underscore values when the SObject is custom.
At present the Lightning c:injector outputs to console on error (unknown binding). Consider different options for this. Display something on screen, show toast, other options?
Currently, the code maps the injector attributes to flow input variables and sets the data type as String
but the code needs to be doing better inference on the injector attribute data type to identify boolean, number, date, etc.
I am trying to deploy force-di as part of a managed package rather than package it as an external dependency. I have included force-di as a submodule and i am trying to deploy the code without modification into a scratch org with a namespace.
The deployment errors because the test suite metadata requires the names of the test classes to be prefixed by the namespace.
I would like the test suite to be modified or removed so that the repo can be deployed into an org with a namespace as is so that i don't have to fork the force-di repo or find some other means to make local changes.
I could copy the code and manually add the namespace prefix but this is a poor developer experience and means we are less likely to use the latest verison of the repo.
Without the namespace prefix i get the following error when deploying the repo into an org with a namespace:
Deploying v58.0 metadata to [email protected] using the v59.0 SOAP API.
Deploy ID: 0Af3G0000116L3qSAE
Status: Failed | ████████████████████████████████████████ | 1/1 Components (Errors:1) | 0/0 Tests (Errors:0)
| Type Name Problem
| ───── ─────────────────── ─────────────────────────────────────────
| Error force_di_teststuite No classes found for di_BindingParamTest.
| Error force_di_teststuite No classes found for di_BindingParamTest.
Is your feature request related to a problem? Please describe.
The givenANewConfigSettingThenGetThatInstance and givenReadOnlyAccessThenGetInstance in the di_PlatformCacheTest require the system permission, Manage Internal Users to insert users with the Read Only or Minimum Access - Salesforce profiles. Without this permission, you will receive the following error:
System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [ProfileId]: [ProfileId]
Describe the solution you'd like
Commented out test methods
Describe alternatives you've considered
Additional context
Add any other context or screenshots about the feature request here.
Create a package per this to ease usage of this library.
In cases where no binding is found it might be preferable to have the injector redirect to the native UI for the given action... ?
You will get a "list index out of bounds" exception if you try to replace a binding that is not present. See di_Binding.Resolver.replaceBindingWith(Object)
method
The current logic around di_Binding.Resolver.get() method first tries to find the bindings from Platform Cache assuming that it is enabled. If those bindings are not found and Platform Cache is enabled, then the assumption is that a new binding must have been introduced into the org since that last cache reload and we should then execute the di_Binding.Resolver.loadBindings() method.
Having said that, there are use cases where bindings are not required. Selector Field Injection is one of those use cases. In that use case, there needs to be a way to designate that a reload to memory of the bindings is not required.
This enhancement request covers that use case.
BindingObjectAlternate__c is not working when I put a value on it. I am using this on the Task object for binding object but cannot find it in the list. So I decided to use BindingObjectAlternate__c but no luck.
When I check the code, It's not included in the select statement and in the if statement. Please see below screenshot:
Please let me know if you know how to fix this.
Thanks and regards,
Divino Brinas
Eventually, we will create a 2GP namespaced package version of this lib (with the di_ prefix removed from its public facing API) as an additional means to consume this lib for those that prefer that. Meanwhile, we will assume early adopters will want to use the library as-is and thus to scope its components using the prefix approach. This also works well for ISV's and Enterprise devs, who may not want to depend on the 2GP namespaced unlocked package, but still, want the segregation a prefix gives in their broader code base.
@afawcett and @douglascayers -- FYI, last week we came across a use case that showed that the MetadataRelationship to EntityDefinition data type for the di_Binding__mdt.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 is most definitely a limitation with the MetadataRelationship EntityDefinition data type and not the Force-DI code. The question becomes how to accommodate these other SObjects from the di_Binding__mdt record.
I can think of two solutions.
A) add a new field on di__Binding__mdt called BindingObjectAlternate__c which would be a TEXT(255) data type. We could allow the admin to use this field to specify an SObject API name not covered by BindingObject__c. We would adjust the code that loads the bindings to use the BindingObject__c field first or the BindingObjectAlternate__c second for the API Binding name. We could also put a validation rule in place to throw an error if both fields were populated.
B) we change the data type of BindingObject__c to be simply TEXT(255) and thus remove the limitation all together.
What are your thoughts on this subject?
Continue to expand out the tests in the /tests sub-folder and those in the sample apps.
because of the critical update regarding createComponent params and the code in the library using dynamic values
there is a risk of exception
Support caching support to improve performance of resolving bindings, particularly when there are dozens to hundreds and the bindings do complex processing.
Subscriber would provide a platform cache name for a partition they make available for this purpose. This will need a new setting somewhere to capture this.
Two main points where to utilize caching:
Cache keys should be the unique name for the binding, and this requires code changes to ensure bindings have a unique name regardless where they are created from (custom metadata or programmatically).
If there are multiple bindings with the same developerName and SObjectType, the regular di_Binding.loadBindings()
works with them as a group. But, when the method calls the di_PlatformCache.addBindingToPlatformCache()
method, it places that one binding into the platform cache using the specific key value. If there are more bindings that match the same developerName and SObjectType, the previous binding is replaced with the new one in the platform cache. Essentially, the last binding added to the cache is the one that remains. This leads to erroneous behavior and errors.
The di_Injector.CustomMetadataModule class makes Schema.describeSObjects() call within a method. The concern is that this is an expensive call and could be optimized if it were outside of the main For Loop.
Tasks:
Describe the bug
duplicate link in README - Advanced Apex Design Patterns by Andy Fawcett should link to https://www.youtube.com/watch?v=BLXp0ZP0cF0
I am trying to use Force-di hence I have a question related to auto-launched or record-triggered flow. How can we manage them using Force-Di? Is there a way or we should not try DI for such flows? Thank you!
And also how we can manage the force-di in an efficient way with frameworks like trigger, logging etc.
Thank you so much,
Hey, first of all, I would like to say thanks for this library, it really saved me a tons of time, great work!
I'm working on switching my org to unlocked packages, so I want to remove compile-time dependencies wherever possible and obviously dependency injection is a great technique to achieve that.
However I came across an issue, and I'm wondering now if my understanding of the problem is correct. I want to separate the implementation of some of my features (let's take a logger as an example) from the interface, as described here https://medium.com/salesforce-architects/5-anti-patterns-in-package-dependency-design-and-how-to-avoid-them-87bb50331cb8 (point 3). What is recommended there is to create package-a containing mock and interface to keep actual implementation separated from the interface.
Let's say that the logger feature (package-b) is depending on multiple different packages, and when a developer wants to use logger in his package, I don't want it to be necessary to install the package with the concrete implementation.
I would like also to be possible in the future to have multiple different orgs with different concrete implementations of the logger. This is why I came to a conclusion that I would like to have some basic implementation in 'package-a' which is mocking the logger both in tests and in normal code execution. Package-a contains a base, mock implementation and custom metadata record, but when the package with concrete implementation is installed on org (package-b), I would like force-di to use that one instead. This is the part where I would see multiple bindings for a single entity (let's say class) to be possible. Right now this is possible for SObject bindings (thanks to field Sequence we can order multiple bindings for some objects). I want to have this option for all bindings not only those related to SObjects.
I know that this could be easily achieved by implementing my own di_Module, but what do you think about making it more 'native'? I created this on my feature branch https://github.com/osieckiAdam/force-di/tree/feature/enable-multiple-bindings-for-developer-name but the reason why I'm not creating a pull request is that I'm not sure if this is a good idea in general to use this multi-binding approach
Describe the bug
Hello! We are trying to optimize our code and is looking at enabling di_Configurations__c.UsePlatformCacheToStoreBindings__c
And we are noticing that the UnitOfWork newInstance is slower when cache is enabled. I might be wrong but it seems that if cache is enabled, it is adding the binding to the cache but not using the cached binding in the succeeding execution?
To Reproduce
I have in total:
In a scratch org, running the code below yields these CPU time results:
fflib_ISObjectUnitOfWork uow = new fflib_SObjectUnitOfWork();
Selector test with 1 id, WHERE ID IN
condition
List<XObject> tickets = XObjectSelector.newInstance().selectByIds(new Set<String> { 'XObjectID' });
Expected behavior
Faster execution is our expected result which we found in both Selector and Domain. Unfortunately the UnitOfWork newInstance is slower when cache is enabled.
Screenshots and text of error observed
Unit of work, cache Disabled
Unit of work, cache Enabled (3rd Execution)
Version
yes, we have the latest
It would be great to be able to reference a version by git tag, for development and dependency management. Is there a reason not to do so?
Versions with no corresponding tags are already cited in sfdx-project.json.
Hi, @ImJohnMDaniel - Good day! I would like to ask if it's possible to use LWC for DI. We are blocked because we cannot call LWC from LComponent because LWC doesn't have auraID to call with. IF it's possible to use LWC for DI, How can we achieve it via code? OR things that we need to consider updating your code to apply to call of LWC?
Cheers!
Thanks and regards,
Dino Brinas
In the di_Binding.Resolver.get()
method, if the di_PlatformCache.getInstance().retrieveBindings(String, Schema.SObjectType)
method does not return matchedBindings and di_Binding.Resolver.bindingsAreRequired
is false (which means "empty bindings are allowed"), then the logic from the loadBindings()
method is never called.
Following my last blog and the code samples in its summary.... I am wondering if an option for the Apex Injector to optionally require a binding if the binding reference (the class) is in-fact also the implementation. This would support simple cases where the developer just wants to use the Injector to add injection for a one off type thats not subclassed., but is a dependency they want to mock.
Human errors and typos are inevitable. I often forget to prefix my Lightning component names with c:
prefix.
This request is for the binding resolver to automatically prefix with c:
if no namespace is provided in the CMDT or programmatic binding.
No namespace means that the binding name does not include a colon :
.
For a given Lightning component whose developer name is c:MyComponent
then both these binding name variants should work:
c:MyComponent
MyComponent
For Lightning components that belong to a specific namespace (not c
), then the full namespace and colon are required.
If you have a binding record that does not bind to an SObject and only binds to the API "developerName", that binding record will be ignored when executing in a namespaced scratch org.
The di_Binding.Resolver.isBindingMatchByFilteringCriteria( di_Binding )
method fails to make a match because the value for this.developerName
is generated from the System.Type
which includes the namespace value but the bind.DeveloperName
doesn't included the namespace because its value was manually entered into the associated CMDT Binding__mdt record.
If you were to enter the namespace along with the API name into the CMDT Binding__mdt record, the resolution would work fine but that in turn limits your interaction with that codebase to only namespace enabled scratch orgs.
Hi there folks.
We have not updated our common library in a while so this just may be an interconnected issue, but I am getting a weird error when creating a new version of the ForceDI library.
I have updated our local repositories (in bitbucket) with the latest code from the common github library. ApexMocks compiled and updated without issue. When I move to ForceDI to update and create a new version, I run it with the --codecoverage parameter and get this error:
Apex Test Failure: Class.di_Binding.ApexBinding.newInstance: line 413, column 1
Class.di_Binding.getInstance: line 82, column 1
Class.di_Binding.getInstance: line 70, column 1
Class.di_BindingTest.givenApexBindingWhenGetNewInstanceThenNewInstance: line 53, column 1 di_Binding.BindingException: Apex binding di_BindingTest.Bob implementation di_BindingTest.Bob does not implement the Provider interface.
Any ideas on what this might be? Is the Provider interface in another library or did I screw up on my steps updating this library?
Thank you.
Eric
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.