diff --git a/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls index a18e1256f40..5e89300a4fb 100644 --- a/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls @@ -50,6 +50,15 @@ public interface fflib_ISObjectUnitOfWork * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) **/ void registerNew(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord); + /** + * Register newly created records to be inserted when commitWork is called, + * it also provides a reference to the single parent record instance (should also be registered as new separately) + * + * @param records newly created records of the same SObjectType to be inserted during commitWork + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + **/ + void registerNew(List records, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord); /** * Register a relationship between two records that have yet to be inserted to the database. This information will be * used during the commitWork phase to make the references only when related records have been inserted to the database. @@ -59,6 +68,15 @@ public interface fflib_ISObjectUnitOfWork * @param relatedTo A SObject instance (yet to be committed to the database) */ void registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo); + /** + * Register a relationship between two records that have yet to be inserted to the database. This information will be + * used during the commitWork phase to make the references only when related records have been inserted to the database. + * + * @param records Existing or newly created records + * @param relatedToField A SObjectField reference to the field that relates the given records to the relatedTo record + * @param relatedTo A SObject instance (yet to be committed to the database) + */ + void registerRelationship(List records, Schema.SObjectField relatedToField, SObject relatedTo); /** * Registers a relationship between a record and a Messaging.Email where the record has yet to be inserted * to the database. This information will be diff --git a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls index 1c38f657b61..9227ca48346 100644 --- a/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls +++ b/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls @@ -242,10 +242,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerEmptyRecycleBin(SObject record) { - String sObjectType = record.getSObjectType().getDescribe().getName(); - assertForSupportedSObjectType(m_emptyRecycleBinMapByType, sObjectType); - - m_emptyRecycleBinMapByType.get(sObjectType).put(record.Id, record); + registerEmptyRecycleBin(new List {record}); } /** @@ -255,9 +252,13 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerEmptyRecycleBin(List records) { - for (SObject record : records) + if (records.isEmpty()) return; + + Map> sortedRecords = + sortAndValidateForExistingNonEvent(m_emptyRecycleBinMapByType, records); + for (String sObjectName : sortedRecords.keySet()) { - registerEmptyRecycleBin(record); + registerEmptyRecycleBin(sObjectName, sortedRecords.get(sObjectName)); } } @@ -278,10 +279,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerNew(List records) { - for (SObject record : records) - { - registerNew(record, null, null); - } + registerNew(records, null, null); } /** @@ -294,35 +292,60 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerNew(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) { - if (record.Id != null) - throw new UnitOfWorkException('Only new records can be registered as new'); - String sObjectType = record.getSObjectType().getDescribe().getName(); + registerNew(new List {record}, relatedToParentField, relatedToParentRecord); + } - assertForNonEventSObjectType(sObjectType); - assertForSupportedSObjectType(m_newListByType, sObjectType); + /** + * Register newly created records to be inserted when commitWork is called, + * it also provides a reference to the single parent record instance (should also be registered as new separately) + * + * @param records newly created records to be inserted during commitWork + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + **/ + public void registerNew(List records, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) + { + if (records.isEmpty()) return; - m_newListByType.get(sObjectType).add(record); - if (relatedToParentRecord!=null && relatedToParentField!=null) - registerRelationship(record, relatedToParentField, relatedToParentRecord); - } + Map> sortedRecords = sortAndValidateForNewNonEvent(m_newListByType, records); + for (String sObjectName : sortedRecords.keySet()) + { + registerNew(sObjectName, sortedRecords.get(sObjectName), relatedToParentField, relatedToParentRecord); + } + } - /** - * Register a relationship between two records that have yet to be inserted to the database. This information will be - * used during the commitWork phase to make the references only when related records have been inserted to the database. - * - * @param record An existing or newly created record - * @param relatedToField A SObjectField reference to the lookup field that relates the two records together - * @param relatedTo A SObject instance (yet to be committed to the database) - */ - public void registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) - { - String sObjectType = record.getSObjectType().getDescribe().getName(); + /** + * Register a relationship between two records that have yet to be inserted to the database. This information will be + * used during the commitWork phase to make the references only when related records have been inserted to the database. + * + * @param record An existing or newly created record + * @param relatedToField A SObjectField reference to the lookup field that relates the two records together + * @param relatedTo A SObject instance (yet to be committed to the database) + */ + public void registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) + { + registerRelationship(new List {record}, relatedToField, relatedTo); + } - assertForNonEventSObjectType(sObjectType); - assertForSupportedSObjectType(m_newListByType, sObjectType); + /** + * Register a relationship between two records that have yet to be inserted to the database. This information will be + * used during the commitWork phase to make the references only when related records have been inserted to the database. + * + * @param records Existing or newly created records + * @param relatedToField A SObjectField reference to the field that relates the given records to the relatedTo record + * @param relatedTo A SObject instance (yet to be committed to the database) + */ + public void registerRelationship(List records, Schema.SObjectField relatedToField, SObject relatedTo) + { + if (records.isEmpty()) return; - m_relationships.get(sObjectType).add(record, relatedToField, relatedTo); - } + Map> sortedRecords = sortAndValidateForNonEvent(m_newListByType, records); + + for (String sObjectName : sortedRecords.keySet()) + { + registerRelationship(sObjectName, sortedRecords.get(sObjectName), relatedToField, relatedTo); + } + } /** * Registers a relationship between a record and a Messaging.Email where the record has yet to be inserted @@ -352,11 +375,12 @@ public virtual class fflib_SObjectUnitOfWork */ public void registerRelationship(SObject record, Schema.SObjectField relatedToField, Schema.SObjectField externalIdField, Object externalId) { - // NOTE: Due to the lack of ExternalID references on Standard Objects, this method can not be provided a standardized Unit Test. - Rick Parker - String sObjectType = record.getSObjectType().getDescribe().getName(); - if(!m_newListByType.containsKey(sObjectType)) - throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType })); - m_relationships.get(sObjectType).add(record, relatedToField, externalIdField, externalId); + // NOTE: Due to the lack of ExternalID references on Standard Objects, this method can not be provided a standardized Unit Test. - Rick Parker + String sObjectType = record.getSObjectType().getDescribe().getName(); + + assertForSupportedSObjectType(m_newListByType, sObjectType); + + m_relationships.get(sObjectType).add(record, relatedToField, externalIdField, externalId); } /** @@ -369,69 +393,66 @@ public virtual class fflib_SObjectUnitOfWork registerDirty(record, new List()); } - /** - * Registers the entire records as dirty or just only the dirty fields if the record was already registered - * - * @param records SObjects to register as dirty - * @param dirtyFields A list of modified fields - */ - public void registerDirty(List records, List dirtyFields) - { - for (SObject record : records) - { - registerDirty(record, dirtyFields); - } - } - /** * Registers the entire record as dirty or just only the dirty fields if the record was already registered * * @param record SObject to register as dirty * @param dirtyFields A list of modified fields */ - public void registerDirty(SObject record, List dirtyFields) - { - if (record.Id == null) - throw new UnitOfWorkException('New records cannot be registered as dirty'); - String sObjectType = record.getSObjectType().getDescribe().getName(); - - assertForNonEventSObjectType(sObjectType); - assertForSupportedSObjectType(m_dirtyMapByType, sObjectType); - - // If record isn't registered as dirty, or no dirty fields to drive a merge - if (!m_dirtyMapByType.get(sObjectType).containsKey(record.Id) || dirtyFields.isEmpty()) - { - // Register the record as dirty - m_dirtyMapByType.get(sObjectType).put(record.Id, record); - } - else - { - // Update the registered record's fields - SObject registeredRecord = m_dirtyMapByType.get(sObjectType).get(record.Id); + public void registerDirty(SObject record, List dirtyFields) + { + registerDirty(new List {record}, dirtyFields); + } - for (SObjectField dirtyField : dirtyFields) { - registeredRecord.put(dirtyField, record.get(dirtyField)); - } + /** + * Registers the entire record as dirty or just only the dirty fields if the record was already registered + * + * @param records SObjects to register as dirty + * @param dirtyFields A list of modified fields + */ + public void registerDirty(List records, List dirtyFields) + { + if (records.isEmpty()) return; - m_dirtyMapByType.get(sObjectType).put(record.Id, registeredRecord); - } - } + Map> sortedRecords = sortAndValidateForExistingNonEvent(m_dirtyMapByType, records); + for (String sObjectName : sortedRecords.keySet()) + { + registerDirty(sObjectName, sortedRecords.get(sObjectName), dirtyFields); + } + } /** * Register an existing record to be updated when commitWork is called, * you may also provide a reference to the parent record instance (should also be registered as new separately) * - * @param record A newly created SObject instance to be inserted during commitWork + * @param record An existing SObject instance to be updated during commitWork * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) **/ public void registerDirty(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) { - registerDirty(record); - if (relatedToParentRecord!=null && relatedToParentField!=null) - registerRelationship(record, relatedToParentField, relatedToParentRecord); + registerDirty(new List {record}, relatedToParentField, relatedToParentRecord); } + /** + * Register existing records to be updated when commitWork is called, + * it provide a reference to the parent record instance (should also be registered as new separately) + * + * @param records Existing records to be updated during commitWork + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + **/ + public void registerDirty(List records, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) + { + if (records.isEmpty()) return; + + Map> sortedRecords = sortAndValidateForExistingNonEvent(m_dirtyMapByType, records); + for (String sObjectName : sortedRecords.keySet()) + { + registerDirty(sObjectName, sortedRecords.get(sObjectName), relatedToParentField, relatedToParentRecord); + } + } + /** * Register a list of existing records to be updated during the commitWork method * @@ -439,10 +460,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerDirty(List records) { - for (SObject record : records) - { - this.registerDirty(record); - } + registerDirty(records, new List()); } /** @@ -452,14 +470,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerUpsert(SObject record) { - if (record.Id == null) - { - registerNew(record, null, null); - } - else - { - registerDirty(record, new List()); - } + registerUpsert(new List {record}); } /** @@ -469,10 +480,15 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerUpsert(List records) { - for (SObject record : records) - { - this.registerUpsert(record); - } + List forInsert = new List(); + List forUpdate = new List(); + for (SObject record : records) + { + if (record.Id == null) forInsert.add(record); + else forUpdate.add(record); + } + if (!forInsert.isEmpty()) registerNew(forInsert); + if (!forUpdate.isEmpty()) registerDirty(forUpdate); } /** @@ -482,14 +498,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerDeleted(SObject record) { - if (record.Id == null) - throw new UnitOfWorkException('New records cannot be registered for deletion'); - String sObjectType = record.getSObjectType().getDescribe().getName(); - - assertForNonEventSObjectType(sObjectType); - assertForSupportedSObjectType(m_deletedMapByType, sObjectType); - - m_deletedMapByType.get(sObjectType).put(record.Id, record); + registerDeleted(new List {record}); } /** @@ -499,10 +508,13 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerDeleted(List records) { - for (SObject record : records) - { - this.registerDeleted(record); - } + if (records.isEmpty()) return; + + Map> sortedRecords = sortAndValidateForExistingNonEvent(m_deletedMapByType, records); + for (String sObjectName : sortedRecords.keySet()) + { + registerDeleted(sObjectName, sortedRecords.get(sObjectName)); + } } /** @@ -523,8 +535,7 @@ public virtual class fflib_SObjectUnitOfWork **/ public void registerPermanentlyDeleted(SObject record) { - this.registerEmptyRecycleBin(record); - this.registerDeleted(record); + registerPermanentlyDeleted(new List {record}); } /** @@ -633,6 +644,129 @@ public virtual class fflib_SObjectUnitOfWork } } + /** + * Adds the records to the delete queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records SObjects to register as deleted + */ + protected void registerDeleted(String sObjectName, List records) + { + m_deletedMapByType.get(sObjectName).putAll(records); + } + + /** + * Adds the records to the dirty and relationship queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records SObjects to register as dirty + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + */ + protected void registerDirty( + String sObjectName, + List records, + Schema.SObjectField relatedToParentField, + SObject relatedToParentRecord) + { + m_dirtyMapByType.get(sObjectName).putAll(records); + + if (relatedToParentRecord == null || relatedToParentField == null) return; + + registerRelationship(sObjectName, records, relatedToParentField, relatedToParentRecord); + } + + /** + * Adds the records to the dirty queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records SObjects to register as dirty + * @param dirtyFields A list of modified fields + */ + protected void registerDirty(String sObjectName, List records, List dirtyFields) + { + // Register the record as dirty, if no dirty fields or all records are not registered as dirty + if (dirtyFields.isEmpty() + || + !m_dirtyMapByType.get(sObjectName).keySet().containsAll(new Map(records).keySet())) + { + m_dirtyMapByType.get(sObjectName).putAll(records); + } + else + { + // Check records one by one and resolve dirty fields. + for (SObject record : records) + { + // Register the record as dirty, if record isn't registered already + if (!m_dirtyMapByType.get(sObjectName).containsKey(record.Id)) + { + m_dirtyMapByType.get(sObjectName).put(record.Id, record); + } + else + { + // Update the registered record's fields + SObject registeredRecord = m_dirtyMapByType.get(sObjectName).get(record.Id); + + for (SObjectField dirtyField : dirtyFields) + { + registeredRecord.put(dirtyField, record.get(dirtyField)); + } + + m_dirtyMapByType.get(sObjectName).put(record.Id, registeredRecord); + } + } + } + } + + /** + * Adds the records to the emptyRecycleBin queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records Records to register as emptyRecycleBin + */ + protected void registerEmptyRecycleBin(String sObjectName, List records) + { + m_emptyRecycleBinMapByType.get(sObjectName).putAll(records); + } + + /** + * Adds the records to the insert queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records Records to register as new, all of the given SObjectType + * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent + * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately) + */ + protected void registerNew( + String sObjectName, + List records, + Schema.SObjectField relatedToParentField, + SObject relatedToParentRecord) + { + m_newListByType.get(sObjectName).addAll(records); + + if (relatedToParentRecord == null || relatedToParentField == null) return; + + registerRelationship(sObjectName, records, relatedToParentField, relatedToParentRecord); + } + + /** + * Adds the records to the relationship queue for the given SObjectType + * + * @param sObjectName The SObjectType name of the records + * @param records Existing or newly created records + * @param relatedToField A SObjectField reference to the field that relates the given records to the relatedTo record + * @param relatedTo A SObject instance (yet to be committed to the database) + */ + protected void registerRelationship( + String sObjectName, + List records, + Schema.SObjectField relatedToField, + SObject relatedTo) + { + m_relationships.get(sObjectName).add(records, relatedToField, relatedTo); + } + private void doCommitWork() { onCommitWorkStarting(); @@ -760,9 +894,7 @@ public virtual class fflib_SObjectUnitOfWork if (sObjectType.length() > 3 && sObjectType.right(3) == '__e') { throw new UnitOfWorkException( - String.format( - 'SObject type {0} must use registerPublishBeforeTransaction or ' + - 'registerPublishAfterTransaction methods to be used within this unit of work', + String.format(Label.fflib_SObjectUnitOfWork_event_SObjectType_must_use_publish, new List { sObjectType } ) ); @@ -775,14 +907,25 @@ public virtual class fflib_SObjectUnitOfWork if (sObjectType.length() > 3 && sObjectType.right(3) != '__e') { throw new UnitOfWorkException( - String.format( - 'SObject type {0} is invalid for publishing within this unit of work', + String.format(Label.fflib_SObjectUnitOfWork_non_event_SObjectType_used_with_publish, new List {sObjectType} ) ); } } + private void assertForExistingRecord(SObject record) + { + if (record.Id == null) + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_new_records_not_supported); + } + + private void assertForNewRecord(SObject record) + { + if (record.Id != null) + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_only_new_records_supported); + } + @TestVisible private void assertForSupportedSObjectType(Map theMap, String sObjectType) { @@ -790,24 +933,92 @@ public virtual class fflib_SObjectUnitOfWork { throw new UnitOfWorkException( String.format( - 'SObject type {0} is not supported by this unit of work', - new List { sObjectType } + Label.fflib_SObjectUnitOfWork_not_supported_SObjectType, + new List {sObjectType} ) ); } } - private class Relationships - { - private List m_relationships = new List(); + private Map> sortAndValidateForExistingNonEvent(Map theMap, List records) + { + Map> result = new Map>(); + for (SObject record : records) + { + String sObjectName = record.getSObjectType().getDescribe().getName(); - public void resolve() - { - // Resolve relationships - for (IRelationship relationship : m_relationships) - { - //relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id); - relationship.resolve(); + assertForSupportedSObjectType(theMap, sObjectName); + assertForExistingRecord(record); + assertForNonEventSObjectType(sObjectName); + + if (result.containsKey(sObjectName)) + { + result.get(sObjectName).add(record); + } + else + { + result.put(sObjectName, new List {record}); + } + } + return result; + } + + private Map> sortAndValidateForNewNonEvent(Map theMap, List records) + { + Map> result = new Map>(); + for (SObject record : records) + { + String sObjectName = record.getSObjectType().getDescribe().getName(); + + assertForSupportedSObjectType(theMap, sObjectName); + assertForNewRecord(record); + assertForNonEventSObjectType(sObjectName); + + if (result.containsKey(sObjectName)) + { + result.get(sObjectName).add(record); + } + else + { + result.put(sObjectName, new List {record}); + } + } + return result; + } + + private Map> sortAndValidateForNonEvent(Map theMap, List records) + { + Map> result = new Map>(); + for (SObject record : records) + { + String sObjectName = record.getSObjectType().getDescribe().getName(); + + assertForSupportedSObjectType(theMap, sObjectName); + assertForNonEventSObjectType(sObjectName); + + if (result.containsKey(sObjectName)) + { + result.get(sObjectName).add(record); + } + else + { + result.put(sObjectName, new List {record}); + } + } + return result; + } + + private class Relationships + { + private List m_relationships = new List(); + + public void resolve() + { + // Resolve relationships + for (IRelationship relationship : m_relationships) + { + //relationship.Record.put(relationship.RelatedToField, relationship.RelatedTo.Id); + relationship.resolve(); } } @@ -815,12 +1026,12 @@ public virtual class fflib_SObjectUnitOfWork public void add(SObject record, Schema.SObjectField relatedToField, Schema.SObjectField externalIdField, Object externalId) { if (relatedToField == null) { - throw new UnitOfWorkException('Invalid argument: relatedToField.'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_invalid_argument_relatedToField_null); } String relationshipName = relatedToField.getDescribe().getRelationshipName(); if (String.isBlank(relationshipName)) { - throw new UnitOfWorkException('Invalid argument: relatedToField. Field supplied is not a relationship field.'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_invalid_argument_relatedToField_invalid); } List relatedObjects = relatedToField.getDescribe().getReferenceTo(); @@ -831,32 +1042,40 @@ public virtual class fflib_SObjectUnitOfWork Boolean externalIdFieldIsValid = externalIdField.getDescribe().isExternalId(); if (!relatedHasExternalIdField) { - throw new UnitOfWorkException('Invalid argument: externalIdField. Field supplied is not a known field on the target sObject.'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_invalid_argument_externalIdField_unknown); } if (!externalIdFieldIsValid) { - throw new UnitOfWorkException('Invalid argument: externalIdField. Field supplied is not a marked as an External Identifier.'); + throw new UnitOfWorkException(Label.fflib_SObjectUnitOfWork_invalid_argument_externalIdField_invalid); } - RelationshipByExternalId relationship = new RelationshipByExternalId(); - relationship.Record = record; - relationship.RelatedToField = relatedToField; - relationship.RelatedTo = relatedObject; - relationship.RelationshipName = relationshipName; - relationship.ExternalIdField = externalIdField; - relationship.ExternalId = externalId; - m_relationships.add(relationship); + RelationshipByExternalId relationship = new RelationshipByExternalId(); + relationship.Record = record; + relationship.RelatedToField = relatedToField; + relationship.RelatedTo = relatedObject; + relationship.RelationshipName = relationshipName; + relationship.ExternalIdField = externalIdField; + relationship.ExternalId = externalId; + m_relationships.add(relationship); } - public void add(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) - { - // Relationship to resolve - Relationship relationship = new Relationship(); - relationship.Record = record; - relationship.RelatedToField = relatedToField; - relationship.RelatedTo = relatedTo; - m_relationships.add(relationship); - } + public void add(List records, Schema.SObjectField relatedToField, SObject relatedTo) + { + for (SObject record : records) + { + add(record, relatedToField, relatedTo); + } + } + + public void add(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) + { + // Relationship to resolve + Relationship relationship = new Relationship(); + relationship.Record = record; + relationship.RelatedToField = relatedToField; + relationship.RelatedTo = relatedTo; + m_relationships.add(relationship); + } public void add(Messaging.SingleEmailMessage email, SObject relatedTo) { diff --git a/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml b/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml index d4002b0a0d3..a78c5ee7ef5 100644 --- a/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml +++ b/sfdx-source/apex-common/main/labels/fflib-Apex-Common-CustomLabels.labels-meta.xml @@ -1,5 +1,75 @@ + + fflib_SObjectUnitOfWork_event_SObjectType_must_use_publish + en_US + false + Error when trying use non-publish method on Event SObjectTypes. + SObject type {0} must use registerPublishBeforeTransaction or registerPublishAfterTransaction methods to be used within this unit of work + + + fflib_SObjectUnitOfWork_invalid_argument_externalIdField_invalid + en_US + false + Error when the provided externalIdField is not an external Id. + Invalid argument: externalIdField. Field supplied is not a marked as an External Identifier. + + + fflib_SObjectUnitOfWork_invalid_argument_externalIdField_unknown + en_US + false + Error when the provided externalIdField is not available on the SObjectType. + Invalid argument: externalIdField. Field supplied is not a known field on the target sObject. + + + fflib_SObjectUnitOfWork_invalid_argument_relatedToField_null + en_US + false + Error when relatedToField argument is null. + Invalid argument: relatedToField. + + + fflib_SObjectUnitOfWork_invalid_argument_relatedToField_invalid + en_US + false + Error when relatedToField argument is not a relationship field. + Invalid argument: relatedToField. Field supplied is not a relationship field. + + + fflib_SObjectUnitOfWork_new_records_not_supported + en_US + false + Error when trying to add new records as dirty or deleted. + New records cannot be registered + + + fflib_SObjectUnitOfWork_non_event_SObjectType_used_with_publish + en_US + false + Error when trying use publish method with a non-Event SObjectType. + SObject type {0} is invalid for publishing within this unit of work + + + fflib_SObjectUnitOfWork_not_supported_SObjectType + en_US + false + Error when adding records of a not supported SObjectType to the UnitOfWork. + SObject type {0} is not supported by this unit of work + + + fflib_SObjectUnitOfWork_only_new_records_supported + en_US + false + Error when adding existing records as new. + Only new records can be registered + + + fflib_SObjectUnitOfWork_require_same_SObjectType + en_US + false + Error when adding records of different SObjectTypes. + All records should be of the same SObjectType + fflib_QueryFactory_crossobject_fieldsets_not_allowed_error en_US diff --git a/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls b/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls index 582e759bc38..6141231e350 100644 --- a/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls +++ b/sfdx-source/apex-common/test/classes/fflib_SObjectUnitOfWorkTest.cls @@ -83,9 +83,8 @@ private with sharing class fflib_SObjectUnitOfWorkTest catch (Exception e) { exceptionThrown = true; - System.assertEquals( - 'Only new records can be registered as new', - e.getMessage(), + System.assert( + e.getMessage().contains(Label.fflib_SObjectUnitOfWork_only_new_records_supported), 'Incorrect exception message thrown' ); } @@ -94,6 +93,26 @@ private with sharing class fflib_SObjectUnitOfWorkTest System.assert(exceptionThrown); } + @IsTest + private static void testRegisterNew_Bulk() + { + List records = new List + { + new Opportunity(Name = 'A'), + new Opportunity(Name = 'B'), + new Opportunity(Name = 'C') + }; + + Test.startTest(); + MockDML mockDML = new MockDML(); + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS, mockDML); + uow.registerNew(records); + uow.commitWork(); + Test.stopTest(); + + System.assertEquals(3, mockDML.recordsForInsert.size()); + } + @IsTest private static void testRegisterDirty_ThrowExceptionOnNewRecord() { @@ -111,7 +130,7 @@ private with sharing class fflib_SObjectUnitOfWorkTest { exceptionThrown = true; System.assertEquals( - 'New records cannot be registered as dirty', + Label.fflib_SObjectUnitOfWork_new_records_not_supported, e.getMessage(), 'Incorrect exception message thrown' ); @@ -212,15 +231,21 @@ private with sharing class fflib_SObjectUnitOfWorkTest { Boolean exceptionThrown = false; fflib_SObjectUnitOfWork unitOfWork = new fflib_SObjectUnitOfWork(MY_SOBJECTS); + final String customObjectName = 'CustomObject__c'; try { - unitOfWork.assertForEventSObjectType('CustomObject__c'); + unitOfWork.assertForEventSObjectType(customObjectName); } catch (Exception e) { exceptionThrown = true; - System.assert( - e.getMessage().contains('invalid for publishing'), + final String errorMessage = + String.format( + Label.fflib_SObjectUnitOfWork_non_event_SObjectType_used_with_publish, + new List {customObjectName}); + System.assertEquals( + errorMessage, + e.getMessage(), 'Incorrect exception message thrown' ); } @@ -240,8 +265,13 @@ private with sharing class fflib_SObjectUnitOfWorkTest catch (Exception e) { exceptionThrown = true; - System.assert( - e.getMessage().contains('not supported by this unit of work'), + final String errorMessage = + String.format( + Label.fflib_SObjectUnitOfWork_not_supported_SObjectType, + new List {'Account'}); + System.assertEquals( + errorMessage, + e.getMessage(), 'Incorrect exception message thrown' ); } @@ -358,7 +388,7 @@ private with sharing class fflib_SObjectUnitOfWorkTest * - Correct events are fired when commitWork fails during DoWork processing * */ - @isTest + @IsTest private static void testDerivedUnitOfWork_CommitDoWorkFail() { // Insert Opportunities with UnitOfWork @@ -460,6 +490,46 @@ private with sharing class fflib_SObjectUnitOfWorkTest System.assertEquals(amountUpdate.Amount, mockDML.recordsForUpdate.get(0).get(Schema.Opportunity.Amount)); } + @IsTest + private static void testRegisterDirtyRecordsWithRelationShip() + { + final Id parentId = fflib_IDGenerator.generate(Account.SObjectType); + final Id opportunityIdA = fflib_IDGenerator.generate(Opportunity.SObjectType); + final Id opportunityIdB = fflib_IDGenerator.generate(Opportunity.SObjectType); + final Account parentRecord = new Account(Id = parentId); + + Opportunity opportunityA = new Opportunity(Id = opportunityIdA); + Opportunity opportunityB = new Opportunity(Id = opportunityIdB); + + MockDML mockDML = new MockDML(); + fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(MY_SOBJECTS, mockDML); + uow.registerDirty( + new List{ opportunityA, opportunityB }, + Schema.Opportunity.AccountId, + parentRecord); + uow.commitWork(); + + System.assertEquals(2, mockDML.recordsForUpdate.size()); + System.assert( + new fflib_MatcherDefinitions.SObjectsWith( + new List>{ + new Map + { + Opportunity.Id => opportunityIdA, + Opportunity.AccountId => parentId + }, + new Map + { + Opportunity.Id => opportunityIdB, + Opportunity.AccountId => parentId + } + } + ) + .matches(mockDML.recordsForUpdate), + 'Records not registered with the correct values' + ); + } + /** * Try registering a single field as dirty on multiple records. * diff --git a/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls b/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls index b9e13ab12cb..ee6fa18b99a 100644 --- a/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls +++ b/sfdx-source/apex-common/test/classes/mocks/fflib_SObjectMocks.cls @@ -102,11 +102,21 @@ public class fflib_SObjectMocks mocks.mockVoidMethod(this, 'registerNew', new List {SObject.class, Schema.sObjectField.class, SObject.class}, new List {record, relatedToParentField, relatedToParentRecord}); } + public void registerNew(List records, SObjectField relatedToParentField, SObject relatedToParentRecord) + { + mocks.mockVoidMethod(this, 'registerNew', new List {SObject.class, Schema.sObjectField.class, SObject.class}, new List {records, relatedToParentField, relatedToParentRecord}); + } + public void registerRelationship(SObject record, Schema.sObjectField relatedToField, SObject relatedTo) { mocks.mockVoidMethod(this, 'registerRelationship', new List {SObject.class, Schema.sObjectField.class, SObject.class}, new List {record, relatedToField, relatedTo}); } + public void registerRelationship(List records, SObjectField relatedToField, SObject relatedTo) + { + mocks.mockVoidMethod(this, 'registerRelationship', new List {SObject.class, Schema.sObjectField.class, SObject.class}, new List {records, relatedToField, relatedTo}); + } + public void registerRelationship(Messaging.SingleEmailMessage email, SObject relatedTo) { mocks.mockVoidMethod(this, 'registerRelationship', new List {Messaging.SingleEmailMessage.class, SObject.class}, new List {email, relatedTo});