diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..522fa4a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. +#ECCN:Open Source diff --git a/README.md b/README.md index 4c6e560..388c96f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,24 @@ +# Using CLI Commands + +Salesforce CLI commands for custom metadata types are available in v49. The custom metadata loader is no longer supported or maintained. As such, Salesforce does not guarantee the functionality or performance of the loader. + +The CLI commands simplify development and help you build automation and synchronize your source from scratch orgs when working with custom metadata types. CLI commands offer more functionality than the custom metadata loader. You can create custom metadata types, generate fields, create records, bulk insert records from a CSV file, and generate custom metadata types from an sObject. In addition, there's no limit on the number of records that can be loaded. + +See the following for more information: + +* [Create and Manage Custom Metadata Types Using CLI](https://help.salesforce.com/articleView?id=custommetadatatypes_cli.htm) +* [cmdt Commands](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_cmdt.htm#cli_reference_force_cmdt) + # Custom Metadata Loader - Deploy to Salesforce +v 3.0 +Custom Metadata tool now supports migration of Custom Settings or Custom Objects to Custom Metadata Types along with migration of records. If you already have Custom Metadata Type, then it can just migrate the Custom Settings/Custom Objects records. + v 2.0 The Custom Metadata loader tool now supports updates of existing custom metadata records. Load the csv file with updates to existing records, and use the tool the import and update the records. # How to use custom metadata loader to update existing records @@ -16,21 +30,86 @@ The Custom Metadata loader tool now supports updates of existing custom metadata v 1.0 -Custom metadata loader is a tool for creating custom metadata records from a csv file. Create custom metadata types in your Salesforce org using Metadata API and then use custom metadata loader to bulk load the records. Behind the scenes, custom metadata loader uses Metadata API to bulk load up to 200 records with a single call. +Custom metadata loader is a tool for creating custom metadata records from a csv file. Create custom metadata types in your Salesforce org using Metadata API and then use custom metadata loader to bulk load the records. Behind the scenes, custom metadata loader uses Metadata API to bulk load up to 200 records with a single call. -Custom metadata loader has a sample custom metadata type CountryMapping__mdt that allows users to map country codes to country names. +Custom metadata loader has a sample custom metadata type CountryMapping__mdt that allows users to map country codes to country names. -#How to deploy custom metadata loader -1. Download the folder custom_md_loader and zip all the files inside this folder. Package.xml should be at the top level of the zipped file. +# How to deploy custom metadata loader +1. Download the folder custom_md_loader and zip all the files inside this folder. Package.xml should be at the top level of the zipped file. 2. Log in to your developer organization via workbench and deploy this zip file. (migration -> deploy) # How to use custom metadata loader 1. Once you have deployed custom metadata loader in your org, assign the permission set 'Custom Metadata Loader' to the users who need to use the tool(See Step 2, 3 on how to assign the perm set) - These users also need the 'Customize Application' to create Custom Metadata records. Admin should have this permission by default. -2. To apply the permission set - CustomMetadataLoader to the user who is using the tool. Go to Administer->Manage Users ->Permission Sets. Click on Custom Metadata Loader. + These users also need the 'Customize Application' to create Custom Metadata records. Admin should have this permission by default. +2. To apply the permission set - CustomMetadataLoader to the user who is using the tool. Go to Administer->Manage Users ->Permission Sets. Click on Custom Metadata Loader. 3 You will be taken to Permission Set page - Click on Manage Assignments. Then click Add Assignments. Choose the user/users. Then click Assign. Then Done. Now the perm set should be successfully assigned. -4. Create a CSV file with a header that contains the field API names, including the org namespace. Either Label or Developer Name is required. A sample csv for CountryMapping__mdt is in the same folder as this README file. -5. Next you are ready to use the tool - Select Custom Metadata Loader from the app menu in your org, then go to the Custom Metadata Loader tab.The app will prompt you to create a remote site setting if it is missing. +4. Create a CSV file with a header that contains the field API names, including the org namespace. Either Label or Developer Name is required. A sample csv for CountryMapping__mdt is in the same folder as this README file. +5. Next you are ready to use the tool - Select Custom Metadata Loader from the app menu in your org, then go to the Custom Metadata Loader tab.The app will prompt you to create a remote site setting if it is missing. 6. Select the CSV file and the corresponding custom metadata type. 7. Click 'Create/Update custom metadata' to bulk load the records from the CSV file into your org. + +# How to use custom metadata migrator + +Use one of the below option to migrate Custom Settings or Custom Objects to Custom Metadata Types. Go to the 'Custom Metadata Migrator' tab + +### Option 1: Migrate Custom Settings/Custom Objects to new Custom Metadata Type + +Input the following: + + --Api name of Custom Setting or Custom Object (e.g. VAT_Settings_CS__c) + --Api name of Custom Metadata Types (e.g. VAT_Settings__mdt) + +Click on 'Migrate' + +### Option 2: Migrate Custom Settings/Custom Objects to existing Custom Metadata Type + +Input the following: + + --Api name of Custom Setting (e.g. VAT_Settings_CS__c) + --Select the name of existing Custom Metadata Types + +Click on 'Migrate' + +### Option 3: Migrate Custom Settings/Custom Objects to existing Custom Metadata Type (using simple mapping) + +Input the following: + + --Api name of Custom Setting.fieldName (e.g. VAT_Settings_CS__c.Active__c) + --Api name of Custom Metadata Types.fieldName (e.g. VAT_Settings__mdt.Active__c) + +Click on 'Migrate' + +### Option 4: Migrate Custom Settings/Custom Objects to existing Custom Metadata Type (using custom mapping) + +Input the following: + + --Api Name of Custom Setting (e.g. VAT_Settings_CS__c) + --Api Name of Custom Metadata Types (e.g. VAT_Settings__mdt) + --Json Mapping (Sample below) + { + "Active__c" : "IsActive__c", + "Timeout__c" : "GlobalTimeout__c", + } + Please note, key should be the Custom Setting/Object field name and that the value is the CMT field name. + +Click on 'Migrate' + +## Custom metadata migrator: more details + +1. Custom metadata migrator provides two different options to do the migration: + - Sync Operation: Migration will happen synchronously. Maximum 200 records can be migrated. + - Async Operation: Migration will happen asynchronously. Maximum 50000 records can be migrated. + To check the status of async migration, go to Deploy -> Deployment Status + +2. Custom Metadata Types label and names + - Custom Setting/Custom Object record name converted into Custom Metadata Types label and name. + - Custom Setting name special character replaced with "_" in Custom Metadata Type names + - If Custom Setting name starts with digit, then Custom Metadata Types name will be prepended with "X" + +3. Custom Settings of type hierarchy not supported. + +4. Custom Objects with field types not supported in Custom Metadata Types not supported. + +5. Currency field on Custom Settings can't be migrated, you can use custom mapping to either avoid mapping or to map to another field. + diff --git a/custom_md_loader/applications/Custom_Metadata_Loader.app b/custom_md_loader/applications/Custom_Metadata_Loader.app index 60220a5..e9a1194 100755 --- a/custom_md_loader/applications/Custom_Metadata_Loader.app +++ b/custom_md_loader/applications/Custom_Metadata_Loader.app @@ -4,4 +4,5 @@ Custom metadata record loader from a CSV file. Custom_Metadata_Loader + CMT_Migrator diff --git a/custom_md_loader/classes/AppConstants.cls b/custom_md_loader/classes/AppConstants.cls index b0a298b..05cb2cc 100644 --- a/custom_md_loader/classes/AppConstants.cls +++ b/custom_md_loader/classes/AppConstants.cls @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ public class AppConstants { public static final String QUALIFIED_API_NAME_ATTRIBUTE = 'QualifiedApiName'; @@ -7,13 +13,12 @@ public class AppConstants { public static final String DESC_ATTRIBUTE = 'Description'; public static final String MDT_SUFFIX = '__mdt'; + public static final String CS_NAME_ATTRIBUTE = 'Name'; public static final String SELECT_STRING = 'Select type'; - - //error messages - public static final String FILE_MISSING = 'Please provide a comma seperated file.'; - public static final String EMPTY_FILE = 'CSV file is empty.'; - public static final String TYPE_OPTION_NOT_SELECTED = 'Please choose a valid custom metadata type.'; public static final String HEADER_MISSING_DEVNAME_AND_LABEL = 'Header must contain atleast one of these two fields - '+ DEV_NAME_ATTRIBUTE + ', ' + LABEL_ATTRIBUTE +'.'; - public static final String INVALID_FILE_ROW_SIZE_DOESNT_MATCH = 'The number of field values does not match the number of header fields on line '; + + // Do not change value for ERROR_UNAUTHORIZED_ENDPOINT + public static final String ERROR_UNAUTHORIZED_ENDPOINT = 'Unauthorized endpoint, please check Setup'; + } \ No newline at end of file diff --git a/custom_md_loader/classes/AppConstants.cls-meta.xml b/custom_md_loader/classes/AppConstants.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/AppConstants.cls-meta.xml +++ b/custom_md_loader/classes/AppConstants.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/CSVFileUtil.cls b/custom_md_loader/classes/CSVFileUtil.cls index 34c345e..d99cbd5 100644 --- a/custom_md_loader/classes/CSVFileUtil.cls +++ b/custom_md_loader/classes/CSVFileUtil.cls @@ -9,7 +9,7 @@ public class CSVFileUtil { //from https://developer.salesforce.com/page/Code_Samples#Parse_a_CSV_with_APEX public static List> parseCSV(Blob csvFileBody,Boolean skipHeaders) { if(csvFileBody == null) { - ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, AppConstants.FILE_MISSING); + ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, Label.FILE_MISSING); ApexPages.addMessage(errorMessage); return null; } diff --git a/custom_md_loader/classes/CSVFileUtil.cls-meta.xml b/custom_md_loader/classes/CSVFileUtil.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/CSVFileUtil.cls-meta.xml +++ b/custom_md_loader/classes/CSVFileUtil.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/CustomMetadataLoaderController.cls-meta.xml b/custom_md_loader/classes/CustomMetadataLoaderController.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/CustomMetadataLoaderController.cls-meta.xml +++ b/custom_md_loader/classes/CustomMetadataLoaderController.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/CustomMetadataLoaderControllerTest.cls-meta.xml b/custom_md_loader/classes/CustomMetadataLoaderControllerTest.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/CustomMetadataLoaderControllerTest.cls-meta.xml +++ b/custom_md_loader/classes/CustomMetadataLoaderControllerTest.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/CustomMetadataUploadController.cls b/custom_md_loader/classes/CustomMetadataUploadController.cls index e236417..44c2351 100644 --- a/custom_md_loader/classes/CustomMetadataUploadController.cls +++ b/custom_md_loader/classes/CustomMetadataUploadController.cls @@ -104,13 +104,13 @@ public class CustomMetadataUploadController { } if(fields == null || (fields != null && fields.size() < 1)) { - ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, AppConstants.EMPTY_FILE); + ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, Label.EMPTY_FILE); ApexPages.addMessage(errorMessage); return; } if(selectedType == AppConstants.SELECT_STRING) { - ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR,AppConstants.TYPE_OPTION_NOT_SELECTED); + ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, Label.TYPE_OPTION_NOT_SELECTED); ApexPages.addMessage(errorMessage); return; } diff --git a/custom_md_loader/classes/CustomMetadataUploadController.cls-meta.xml b/custom_md_loader/classes/CustomMetadataUploadController.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/CustomMetadataUploadController.cls-meta.xml +++ b/custom_md_loader/classes/CustomMetadataUploadController.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/CustomMetadataUploadControllerTest.cls b/custom_md_loader/classes/CustomMetadataUploadControllerTest.cls index 49106e4..58faa98 100644 --- a/custom_md_loader/classes/CustomMetadataUploadControllerTest.cls +++ b/custom_md_loader/classes/CustomMetadataUploadControllerTest.cls @@ -10,17 +10,17 @@ public class CustomMetadataUploadControllerTest { public static testmethod void testUploadNoFile() { CustomMetadataUploadController ctrl = setup(null); - invokeCreateCMAndValidateError(ctrl, AppConstants.FILE_MISSING); + invokeCreateCMAndValidateError(ctrl, Label.FILE_MISSING); } public static testmethod void testUploadEmptyFile() { CustomMetadataUploadController ctrl = setup(Blob.valueOf('')); - invokeCreateCMAndValidateError(ctrl, AppConstants.EMPTY_FILE); + invokeCreateCMAndValidateError(ctrl, Label.EMPTY_FILE); } public static testmethod void testSelectedTypeMissing() { CustomMetadataUploadController ctrl = setup(Blob.valueOf('Text__c'), AppConstants.SELECT_STRING); - invokeCreateCMAndValidateError(ctrl, AppConstants.TYPE_OPTION_NOT_SELECTED); + invokeCreateCMAndValidateError(ctrl, Label.TYPE_OPTION_NOT_SELECTED); } public static testmethod void testInvalidHeaderMissingFields() { @@ -61,7 +61,7 @@ public class CustomMetadataUploadControllerTest { String countryLabel = 'AmericaTest'+Math.random(); CustomMetadataUploadController ctrl = setup(Blob.valueOf('DeveloperName,CountryCode__c,CountryName__c\n'+countryLabel+',US')); - invokeCreateCMAndValidateError(ctrl, AppConstants.INVALID_FILE_ROW_SIZE_DOESNT_MATCH + '1'); + invokeCreateCMAndValidateError(ctrl, System.Label.INVALID_FILE_ROW_SIZE_DOESNT_MATCH + '1'); } static CustomMetadataUploadController setup(Blob file) { diff --git a/custom_md_loader/classes/CustomMetadataUploadControllerTest.cls-meta.xml b/custom_md_loader/classes/CustomMetadataUploadControllerTest.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/CustomMetadataUploadControllerTest.cls-meta.xml +++ b/custom_md_loader/classes/CustomMetadataUploadControllerTest.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/JsonUtilities.cls b/custom_md_loader/classes/JsonUtilities.cls new file mode 100644 index 0000000..791889e --- /dev/null +++ b/custom_md_loader/classes/JsonUtilities.cls @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * Utilities class for common manipulation of json format data + * + */ +public with sharing class JsonUtilities { + + public class JsonUtilException extends Exception {} + + /** + * This basic method takes a string formatted as json and returns a map + * containing the name/value pairs. If the input is empty or is not formatted correctly + * the method throws a JsonUtilException exception. + **/ + Public static Map getValuesFromJson(String jsonString) { + Map jsonObjMap; + Map jsonMap = new Map(); + if (String.isBlank(jsonString)){ + throw new JsonUtilException(Label.ERROR_JSON_EMPTY); + } + try { + jsonObjMap = (Map)JSON.deserializeUntyped(jsonString); + if(jsonObjMap == null || jsonObjMap.size() == 0) { + throw new JsonUtilException(Label.ERROR_JSON_EMPTY); + } else { + for (String pKey : jsonObjMap.keySet() ) { + try { + String pVal = (String)jsonObjMap.get(pKey); + jsonMap.put(pKey, pVal); + } catch (exception e) { + throw new JsonUtilException(Label.ERROR_JSON_BAD_FORMAT, e); + } + } + } + return jsonMap; + } catch (Exception e) { + throw new JsonUtilException(Label.ERROR_JSON_BAD_FORMAT, e); + } + } + + +} diff --git a/custom_md_loader/classes/JsonUtilities.cls-meta.xml b/custom_md_loader/classes/JsonUtilities.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/JsonUtilities.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MDWrapperWebServiceMock.cls-meta.xml b/custom_md_loader/classes/MDWrapperWebServiceMock.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/MDWrapperWebServiceMock.cls-meta.xml +++ b/custom_md_loader/classes/MDWrapperWebServiceMock.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/MetadataApexApiLoader.cls b/custom_md_loader/classes/MetadataApexApiLoader.cls new file mode 100644 index 0000000..088a724 --- /dev/null +++ b/custom_md_loader/classes/MetadataApexApiLoader.cls @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public with sharing class MetadataApexApiLoader extends MetadataLoader { + + // TODO: MetadataDeployStatus: We may use in future to provide status on UI (polling or email) + public MetadataDeployStatus mdDeployStatus {get;set;} + // TODO: MetadataDeployCallback: We may use in future to provide status on UI (polling or email) + public MetadataDeployCallback callback {get;set;} + + public MetadataApexApiLoader() { + this.mdDeployStatus = new MetadataApexApiLoader.MetadataDeployStatus(); + this.callback = new MetadataDeployCallback(); + } + + public MetadataApexApiLoader.MetadataDeployStatus getMdDeployStatus() { + return this.mdDeployStatus; + } + + public MetadataApexApiLoader.MetadataDeployCallback getCallback() { + return this.callback; + } + + public override void migrateAsIsWithObjCreation(String csName, String cmtName) { + List messages = new List(); + messages.add(new MetadataResponse.Message(100, Label.MSG_NOT_SUPPORTED)); + response.setIsSuccess(false); + response.setMessages(messages); + } + + public override void migrateAsIsMapping(String csName, String cmtName) { + super.migrateAsIsMapping(csName, cmtName); + buildResponse(); + } + + public override void migrateSimpleMapping(String csNameWithField, String cmtNameWithField) { + super.migrateSimpleMapping(csNameWithField, cmtNameWithField); + buildResponse(); + } + + public override void migrateCustomMapping(String csName, String cmtName, String mapping) { + super.migrateCustomMapping(csName, cmtName, mapping); + buildResponse(); + } + + private void buildResponse() { + if(response.IsSuccess()) { + List messages = new List(); + messages.add(new MetadataResponse.Message(100, Label.MSG_MIGRATION_IN_PROGRESS + getMdDeployStatus().getJobId())); + response.setIsSuccess(true); + response.setMessages(messages); + } + } + + public override void migrate(MetadataMappingInfo mappingInfo) { + System.debug('MetadataApexApiLoader.migrate -->'); + try { + Map descFieldResultMap = mappingInfo.getSrcFieldResultMap(); + String typeDevName = mappingInfo.getCustomMetadadataTypeName() + .subString(0, mappingInfo.getCustomMetadadataTypeName().indexOf(AppConstants.MDT_SUFFIX)); + List records = new List(); + for(sObject csRecord : mappingInfo.getRecordList()) { + + Metadata.CustomMetadata customMetadataRecord = new Metadata.CustomMetadata(); + customMetadataRecord.values = new List(); + + if(csRecord.get(AppConstants.CS_NAME_ATTRIBUTE) != null) { + String strippedLabel = (String)csRecord.get(AppConstants.CS_NAME_ATTRIBUTE); + String tempVal = strippedLabel.substring(0, 1); + + if(tempVal.isNumeric()) { + strippedLabel = 'X' + strippedLabel; + } + strippedLabel = strippedLabel.replaceAll('\\W+', '_').replaceAll('__+', '_').replaceAll('\\A[^a-zA-Z]+', '').replaceAll('_$', ''); + System.debug('strippedLabel ->' + strippedLabel); + + // default fullName to type_dev_name.label + customMetadataRecord.fullName = typeDevName + '.'+ strippedLabel; + customMetadataRecord.label = (String)csRecord.get(AppConstants.CS_NAME_ATTRIBUTE); + } + for(String fieldName : mappingInfo.getCSToMDT_fieldMapping().keySet()) { + Schema.DescribeFieldResult descCSFieldResult = descFieldResultMap.get(fieldName.toLowerCase()); + + if(mappingInfo.getCSToMDT_fieldMapping().get(fieldName).endsWith('__c')) { + Metadata.CustomMetadataValue cmv = new Metadata.CustomMetadataValue(); + cmv.field = mappingInfo.getCSToMDT_fieldMapping().get(fieldName); + cmv.value = csRecord.get(fieldName); + customMetadataRecord.values.add(cmv); + } + } + records.add(customMetadataRecord); + } + + callback.setMdDeployStatus(mdDeployStatus); + + Metadata.DeployContainer deployContainer = new Metadata.DeployContainer(); + for(Metadata.CustomMetadata record : records) { + deployContainer.addMetadata(record); + } + + // Enqueue custom metadata deployment + Id jobId = Metadata.Operations.enqueueDeployment(deployContainer, callback); + mdDeployStatus.setJobId(jobId); + } + catch (Exception e) { + System.debug('MetadataApexApiLoader.Error Message=' + e.getMessage()); + List messages = new List(); + messages.add(new MetadataResponse.Message(100, e.getMessage())); + + response.setIsSuccess(false); + response.setMessages(messages); + } + } + + // TODO: Status is still a work in progress. In future, we may use this to provide + // status on UI (polling or email) + public class MetadataDeployStatus { + public Id jobId {get;set;} + public Metadata.DeployStatus deployStatus {get;set;} + public boolean success {get;set;} + + public MetadataDeployStatus() {} + + public Id getJobId() { + return this.jobId; + } + public void setJobId(Id jobId) { + this.jobId = jobId; + } + + public Metadata.DeployStatus getDeployStatus() { + return this.deployStatus; + } + public void setDeployStatus(Metadata.DeployStatus deployStatus) { + this.deployStatus = deployStatus; + } + + public boolean getSuccess() { + return this.success; + } + public void setSuccess(boolean success) { + this.success = success; + } + } + + // TODO: Callback is still a work in progress. In future, we may use this to provide + // status on UI (polling or email) + public class MetadataDeployCallback implements Metadata.DeployCallback { + + public MetadataApexApiLoader.MetadataDeployStatus mdDeployStatus {get;set;} + + public void setMdDeployStatus(MetadataApexApiLoader.MetadataDeployStatus mdDeployStatus) { + this.mdDeployStatus = mdDeployStatus; + } + + public MetadataDeployCallback() { + } + + public void handleResult(Metadata.DeployResult result, + Metadata.DeployCallbackContext context) { + + if (result.status == Metadata.DeployStatus.Succeeded) { + mdDeployStatus.setSuccess(true); + mdDeployStatus.setDeployStatus(result.status); + } + else if (result.status == Metadata.DeployStatus.InProgress) { + // Deployment In Progress + mdDeployStatus.setSuccess(false); + mdDeployStatus.setDeployStatus(result.status); + } + else { + mdDeployStatus.setSuccess(false); + mdDeployStatus.setDeployStatus(result.status); + // Deployment was not successful + } + } + } + +} diff --git a/custom_md_loader/classes/MetadataApexApiLoader.cls-meta.xml b/custom_md_loader/classes/MetadataApexApiLoader.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataApexApiLoader.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataLoader.cls b/custom_md_loader/classes/MetadataLoader.cls new file mode 100644 index 0000000..a7c087c --- /dev/null +++ b/custom_md_loader/classes/MetadataLoader.cls @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public virtual with sharing class MetadataLoader { + + public enum MetadataOpType { APEXWRAPPER, METADATAAPEX } + + public MetadataResponse response; + public MetadataMapperDefault mapper; + + public MetadataLoader() { + response = new MetadataResponse(true, null, null); + } + + /** + * This will first create custom object and then migrates the records. + * This assumes that Custom Setting and MDT have the same API field names and + * same field data type. + * csName: Label, DeveloperName, Description (We might need it for migration) + * cmtName: e.g. VAT_Settings if mdt is 'VAT_Settings__mdt' + */ + public virtual void migrateAsIsWithObjCreation(String csName, String cmtName) { + MetadataMappingInfo mappingInfo = null; + try { + mapper = MetadataMapperFactory.getMapper(MetadataMapperType.ASIS); + mappingInfo = mapper.mapper(csName, cmtName, null); + } + catch (Exception e) { + System.debug('MetadataLoader.Error Message=' + e.getMessage()); + List messages = new List(); + messages.add(new MetadataResponse.Message(100, Label.MSG_CUSTOM_SETTINGS_EXISTS)); + messages.add(new MetadataResponse.Message(100, Label.MSG_CS_MDT_REQ)); + messages.add(new MetadataResponse.Message(200, Label.MSG_CHECK_API_NAMES)); + messages.add(new MetadataResponse.Message(300, e.getMessage())); + response.setIsSuccess(false); + response.setMessages(messages); + } + } + + /** + * This assumes that CS and MDT have the same API field names. + * + * csName: Label, DeveloperName, Description (We might need it for migration) + * cmtName: e.g. VAT_Settings if mdt is 'VAT_Settings__mdt' + */ + public virtual void migrateAsIsMapping(String csName, String cmtName) { + MetadataMappingInfo mappingInfo = null; + try { + mapper = MetadataMapperFactory.getMapper(MetadataMapperType.ASIS); + mappingInfo = mapper.mapper(csName, cmtName, null); + migrate(mappingInfo); + } + catch (Exception e) { + System.debug('MetadataLoader.Error Message=' + e.getMessage()); + List messages = new List(); + messages.add(new MetadataResponse.Message(100, Label.MSG_CS_MDT_EXISTS)); + messages.add(new MetadataResponse.Message(100, Label.MSG_CS_MDT_REQ)); + messages.add(new MetadataResponse.Message(200, Label.MSG_CHECK_API_NAMES)); + messages.add(new MetadataResponse.Message(300, e.getMessage())); + response.setIsSuccess(false); + response.setMessages(messages); + return; + } + } + + /** + * csNameAndField: Label, DeveloperName, Description (We might need it for migration) + * cmtNameAndField: e.g. VAT_Settings if mdt is 'VAT_Settings__mdt' + */ + public virtual void migrateSimpleMapping(String csNameAndField, String cmtNameAndField) { + MetadataMappingInfo mappingInfo = null; + try { + mapper = MetadataMapperFactory.getMapper(MetadataMapperType.SIMPLE); + mappingInfo = mapper.mapper(csNameAndField, cmtNameAndField, null); + migrate(mappingInfo); + } + catch (Exception e) { + System.debug('MetadataLoader.Error Message=' + e.getMessage()); + List messages = new List(); + messages.add(new MetadataResponse.Message(100, Label.MSG_CS_MDT_EXISTS)); + messages.add(new MetadataResponse.Message(100, Label.MSG_CS_MDT_FIELD_NAME_REQ)); + messages.add(new MetadataResponse.Message(200, Label.MSG_CS_MDT_FIELD_NAME_FORMAT)); + messages.add(new MetadataResponse.Message(300, Label.MSG_CHECK_API_NAMES)); + messages.add(new MetadataResponse.Message(400, e.getMessage())); + response.setIsSuccess(false); + response.setMessages(messages); + return; + } + } + + /** + * This assumes that CS and MDT have the same API field names. + * + * csName: Label, DeveloperName, Description (We might need it for migration) + * cmtName: e.g. VAT_Settings if mdt is 'VAT_Settings__mdt' + * mapping: e.g. Json mapping between CS field Api and CMT field Api names + */ + public virtual void migrateCustomMapping(String csName, String cmtName, String mapping) { + MetadataMappingInfo mappingInfo = null; + try { + mapper = MetadataMapperFactory.getMapper(MetadataMapperType.CUSTOM); + mappingInfo = mapper.mapper(csName, cmtName, mapping); + migrate(mappingInfo); + } + catch (Exception e) { + System.debug('MetadataLoader.Error Message=' + e.getMessage()); + List messages = new List(); + messages.add(new MetadataResponse.Message(100, Label.MSG_CS_MDT_EXISTS)); + messages.add(new MetadataResponse.Message(100, Label.MSG_CS_MDT_MAPPING_REQ)); + messages.add(new MetadataResponse.Message(200, Label.MSG_CHECK_API_NAMES)); + messages.add(new MetadataResponse.Message(200, Label.MSG_JSON_FORMAT)); + messages.add(new MetadataResponse.Message(300, Label.MSG_JSON_API_NAMES)); + messages.add(new MetadataResponse.Message(400, e.getMessage())); + response.setIsSuccess(false); + response.setMessages(messages); + return; + } + } + + public virtual void migrate(MetadataMappingInfo mappingInfo) { + } + + public MetadataMapperDefault getMapper() { + return mapper; + } + + public MetadataResponse getMetadataResponse() { + return response; + } + +} diff --git a/custom_md_loader/classes/MetadataLoader.cls-meta.xml b/custom_md_loader/classes/MetadataLoader.cls-meta.xml new file mode 100644 index 0000000..add07b2 --- /dev/null +++ b/custom_md_loader/classes/MetadataLoader.cls-meta.xml @@ -0,0 +1,5 @@ + + + 34.0 + Active + diff --git a/custom_md_loader/classes/MetadataLoaderClient.cls b/custom_md_loader/classes/MetadataLoaderClient.cls new file mode 100644 index 0000000..59f2018 --- /dev/null +++ b/custom_md_loader/classes/MetadataLoaderClient.cls @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * Please note this is just a sample usage class. You need to replace hard-coded + * values with real values. + * + **/ +public class MetadataLoaderClient { + + public void migrateMetatdataApex() { + } + + /********************************************************* + * Desc This will create custom object and then migrate Custom Settings data + * to Custom Metadata Types records as is + * + * @param Name + * of Custom Setting Api (VAT_Settings_CS__c) + * @param Name + * of Custom Metadata Types Api (VAT_Settings__mdt) + * @return + *********************************************************/ + public void migrateAsIsWithObjCreation(String csName, String mdtName) { + MetadataLoader loader = MetadataLoaderFactory.getLoader(MetadataOpType.APEXWRAPPER); + loader.migrateAsIsWithObjCreation(csName, mdtName); + } + + /********************************************************* + * Desc Migrate Custom Settings data to Custom Metadata Types records as is + * + * @param Name + * of Custom Setting Api (VAT_Settings_CS__c) + * @param Name + * of Custom Metadata Types Api (VAT_Settings__mdt) + * @return + *********************************************************/ + public void migrateAsIsMapping(String csName, String mdtName) { + MetadataLoader loader = MetadataLoaderFactory.getLoader(MetadataOpType.APEXWRAPPER); + loader.migrateAsIsMapping(csName, mdtName); + } + + /********************************************************* + * Desc Migrate Custom Settings data to Custom Metadata Types records if you + * have only one field mapping + * + * @param Name + * of Custom Setting Api.fieldName (VAT_Settings_CS__c.Active__c) + * @param Name + * of Custom Metadata Types Api.fieldMame + * (VAT_Settings__mdt.IsActive__c) + * @return + *********************************************************/ + public void migrateSimpleMapping(String csNameWithField, + String mdtNameWithField) { + MetadataLoader loader = MetadataLoaderFactory.getLoader(MetadataOpType.APEXWRAPPER); + loader.migrateSimpleMapping(csNameWithField, mdtNameWithField); + } + + /********************************************************* + * Desc Migrate Custom Settings data to Custom Metadata Types records if you + * have only different Api names in Custom Settings and Custom Metadata + * Types + * + * @param Name + * of Custom Setting Api (VAT_Settings_CS__c) + * @param Name + * of Custom Metadata Types Api (VAT_Settings__mdt) + * @param Json + * Mapping (Sample below) { "Active__c" : "IsActive__c", + * "Timeout__c" : "GlobalTimeout__c", "EndPointURL__c" : + * "URL__c", } + * + * @return + *********************************************************/ + public void migrateCustomMapping(String csName, String mdtName, + String jsonMapping) { + MetadataLoader loader = MetadataLoaderFactory.getLoader(MetadataOpType.APEXWRAPPER); + loader.migrateCustomMapping(csName, mdtName, jsonMapping); + } + +} diff --git a/custom_md_loader/classes/MetadataLoaderClient.cls-meta.xml b/custom_md_loader/classes/MetadataLoaderClient.cls-meta.xml new file mode 100644 index 0000000..add07b2 --- /dev/null +++ b/custom_md_loader/classes/MetadataLoaderClient.cls-meta.xml @@ -0,0 +1,5 @@ + + + 34.0 + Active + diff --git a/custom_md_loader/classes/MetadataLoaderFactory.cls b/custom_md_loader/classes/MetadataLoaderFactory.cls new file mode 100644 index 0000000..27de8e1 --- /dev/null +++ b/custom_md_loader/classes/MetadataLoaderFactory.cls @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public with sharing class MetadataLoaderFactory { + + public static MetadataLoader getLoader(MetadataOpType mt) { + MetadataLoader loader = null; + + if(mt == MetadataOpType.APEXWRAPPER) { + loader = new MetadataWrapperApiLoader(); + } + if(mt == MetadataOpType.METADATAAPEX) { + loader = new MetadataApexApiLoader(); + } + return loader; + } + +} diff --git a/custom_md_loader/classes/MetadataLoaderFactory.cls-meta.xml b/custom_md_loader/classes/MetadataLoaderFactory.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataLoaderFactory.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataMapper.cls b/custom_md_loader/classes/MetadataMapper.cls new file mode 100644 index 0000000..84dbfa7 --- /dev/null +++ b/custom_md_loader/classes/MetadataMapper.cls @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * Interface for mapping between source object and target object fields. + * Implementation of this interface will handle the mapping between source + * and target object fields. + * + * */ +public interface MetadataMapper { + + /** + * Maps the source fields with target fields. + * + * @param sFrom: source object + * @param sFrom: target object + * @param mapping: optional param, required for custom mapping in the form of json. + * */ + MetadataMappingInfo mapper(String sFrom, String sTo, String mapping); + + // TODO: Currently, this is not implemented, but I think we should implement to + // validate the fields that are not supported by Custom Metadata Types. + + /** + * Validate the fields between source and target object. + * e.g. If source Custom Object is having a field of type 'masterdetail', + * then we should flag it an error or warning? + * + * */ + boolean validate(); + + /** + * Map for source-target field mapping + * + * */ + void mapSourceTarget(); +} diff --git a/custom_md_loader/classes/MetadataMapper.cls-meta.xml b/custom_md_loader/classes/MetadataMapper.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataMapper.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataMapperCustom.cls b/custom_md_loader/classes/MetadataMapperCustom.cls new file mode 100644 index 0000000..31a06c2 --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperCustom.cls @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * Custom mapping between source and target object fields based on json. + * + * */ +public with sharing class MetadataMapperCustom extends MetadataMapperDefault { + + private String csFieldName; + private String mdtFieldName; + private Map fieldsMap; + + public MetadataMapperCustom() { + super(); + } + + /** + * Maps the source fields with target fields. + * + * @param sFrom: source object, e.g. VAT_Settings__c + * @param sFrom: target object, e.g. VAT_Settings__mdt + * @param mapping: json mapping, e.g. {"Field_cs_1__c", "Field_mdt_1__c"} + * */ + public override MetadataMappingInfo mapper(String sFrom, String sTo, String mapping) { + try { + fetchSourceMetadataAndRecords(sFrom, sTo, mapping); + mapSourceTarget(); + } + catch (Exception e) { + throw e; + } + return mappingInfo; + } + + /** + * Fetches source object metadata and builds the mapping info + */ + private void fetchSourceMetadataAndRecords(String csName, String mdtName, String mapping) { + if(!mdtName.endsWith(AppConstants.MDT_SUFFIX)) { + throw new MetadataMigrationException(Label.MSG_MDT_END + AppConstants.MDT_SUFFIX); + } + + List srcFieldNames = new List(); + Map srcFieldResultMap = new Map(); + + try { + mappingInfo.setCustomSettingName(csName); + mappingInfo.setCustomMetadadataTypeName(mdtName); + + DescribeSObjectResult objDef = Schema.getGlobalDescribe().get(csName).getDescribe(); + Map fields = objDef.fields.getMap(); + + this.fieldsMap = JsonUtilities.getValuesFromJson(mapping); + + for(String fieldName: fieldsMap.keySet()) { + srcFieldNames.add(fieldName); + DescribeFieldResult fieldDesc = fields.get(fieldName).getDescribe(); + srcFieldResultMap.put(fieldName.toLowerCase(), fieldDesc); + } + + String selectClause = 'SELECT ' + String.join(srcFieldNames, ', ') + ' ,Name '; + String query = selectClause + ' FROM ' + csName + ' LIMIT 50000'; + + List recordList = Database.query(query); + + mappingInfo.setSrcFieldNames(srcFieldNames); + mappingInfo.setRecordList(recordList); + mappingInfo.setSrcFieldResultMap(srcFieldResultMap); + } + catch (Exception e) { + System.debug('MetadataMapperCustom.Error Message=' + e.getMessage()); + throw e; + } + + } + + // TODO: Currently, this is not implemented (well defaulted to true), but I think + // we should implement to validate the fields that are not supported by Custom Metadata Types. + + /** + * Validate the fields between source and target object. + * e.g. If source Custom Object is having a field of type 'masterdetail', + * then we should flag it an error or warning? + * + * */ + public override boolean validate(){ + return true; + } + + /** + * Map for source-target field mapping + * + * */ + public override void mapSourceTarget() { + mappingInfo.setCSToMDT_fieldMapping(this.fieldsMap); + } + +} diff --git a/custom_md_loader/classes/MetadataMapperCustom.cls-meta.xml b/custom_md_loader/classes/MetadataMapperCustom.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperCustom.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataMapperDefault.cls b/custom_md_loader/classes/MetadataMapperDefault.cls new file mode 100644 index 0000000..64fbf1c --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperDefault.cls @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * Default mapping between source and target object fields based on json. + * This assumes that source object and target object are having exactly same + * fields metadata. + * + * */ +public virtual with sharing class MetadataMapperDefault implements MetadataMapper { + + protected MetadataMappingInfo mappingInfo; + private List srcFieldNames; + + public MetadataMapperDefault() { + this.mappingInfo = new MetadataMappingInfo(); + } + + /** + * Maps the source fields with target fields. + * + * @param sFrom: source object + * @param sFrom: target object + * @param mapping: optional param, in this case, its null + * */ + public virtual MetadataMappingInfo mapper(String sFrom, String sTo, String mapping) { + try { + mappingInfo.setCustomSettingName(sFrom); + mappingInfo.setCustomMetadadataTypeName(sTo); + + fetchSourceMetadataAndRecords(sFrom); + mapSourceTarget(); + } + catch (Exception e) { + throw e; + } + return mappingInfo; + } + + /** + * Fetches source object metadata and builds the mapping info + */ + private void fetchSourceMetadataAndRecords(String customSettingApiName) { + + if(!mappingInfo.getCustomMetadadataTypeName().endsWith(AppConstants.MDT_SUFFIX)) { + throw new MetadataMigrationException(Label.MSG_MDT_END + AppConstants.MDT_SUFFIX); + } + + srcFieldNames = new List(); + Map srcFieldResultMap = new Map(); + + try { + DescribeSObjectResult objDef = Schema.getGlobalDescribe().get(customSettingApiName).getDescribe(); + Map fields = objDef.fields.getMap(); + + String selectFields = ''; + for(String fieldName : fields.keySet()) { + DescribeFieldResult fieldDesc = fields.get(fieldName).getDescribe(); + String fieldQualifiedApiName = fieldDesc.getName(); + if(fieldQualifiedApiName.endsWith('__c')) { + srcFieldNames.add(fieldQualifiedApiName); + } + srcFieldResultMap.put(fieldName.toLowerCase(), fieldDesc); + + } + + String selectClause = 'SELECT ' + String.join(srcFieldNames, ', ') + ' ,Name '; + String query = selectClause + ' FROM ' + customSettingApiName + ' LIMIT 50000'; + List recordList = Database.query(query); + + mappingInfo.setSrcFieldNames(srcFieldNames); + mappingInfo.setRecordList(recordList); + mappingInfo.setSrcFieldResultMap(srcFieldResultMap); + } + catch (Exception e) { + System.debug('MetadataMapperDefault.Error Message=' + e.getMessage()); + throw e; + } + } + + // TODO: Currently, this is not implemented (well defaulted to true), but I think + // we should implement to validate the fields that are not supported by Custom Metadata Types. + + /** + * Validate the fields between source and target object. + * e.g. If source Custom Object is having a field of type 'masterdetail', + * then we should flag it an error or warning? + * + * */ + public virtual boolean validate(){ + return true; + } + + /** + * Map for source-target field mapping + * + * */ + public virtual void mapSourceTarget() { + Map csToMDT_fieldMapping = mappingInfo.getCSToMDT_fieldMapping(); + for(String fieldName: srcFieldNames) { + csToMDT_fieldMapping.put(fieldName, fieldName); + } + } + + public MetadataMappingInfo getMappingInfo() { + return mappingInfo; + } + +} diff --git a/custom_md_loader/classes/MetadataMapperDefault.cls-meta.xml b/custom_md_loader/classes/MetadataMapperDefault.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperDefault.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataMapperFactory.cls b/custom_md_loader/classes/MetadataMapperFactory.cls new file mode 100644 index 0000000..2682ad9 --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperFactory.cls @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public with sharing class MetadataMapperFactory { + + public static MetadataMapperDefault getMapper(MetadataMapperType mt) { + MetadataMapperDefault mapper = null; + if(mt == MetadataMapperType.ASIS) { + mapper = new MetadataMapperDefault(); + } + if(mt == MetadataMapperType.SIMPLE) { + mapper = new MetadataMapperSimple(); + } + else if(mt == MetadataMapperType.CUSTOM) { + mapper = new MetadataMapperCustom(); + } + return mapper; + } + +} diff --git a/custom_md_loader/classes/MetadataMapperFactory.cls-meta.xml b/custom_md_loader/classes/MetadataMapperFactory.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperFactory.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataMapperSimple.cls b/custom_md_loader/classes/MetadataMapperSimple.cls new file mode 100644 index 0000000..b06aa2d --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperSimple.cls @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * Simple mapping between source and target object field. + * This option is for use-cases where you have only one field in source/target. + * + * */ +public with sharing class MetadataMapperSimple extends MetadataMapperDefault { + + private String csFieldName; + private String mdtFieldName; + + public MetadataMapperSimple() { + super(); + } + + /** + * Maps the source fields with target fields. + * + * @param sFrom: source object, e.g. VAT_Settings__c.IsActive__c + * @param sFrom: target object, e.g. VAT_Settings__mdt.Active__c + * @param mapping: in this case, mapping is null + * */ + public override MetadataMappingInfo mapper(String csName, String cmtName, String mapping) { + fetchSourceMetadataAndRecords(csName, cmtName); + mapSourceTarget(); + + return mappingInfo; + } + + /** + * Fetches source object metadata and builds the mapping info + */ + private void fetchSourceMetadataAndRecords(String csNameWithField, String mdtNameWithField) { + try { + List srcFieldNames = new List(); + Map srcFieldResultMap = new Map(); + + String[] csArray = csNameWithField.split('\\.'); + String[] mdtArray = mdtNameWithField.split('\\.'); + + mappingInfo.setCustomSettingName(csArray[0]); + mappingInfo.setCustomMetadadataTypeName(mdtArray[0]); + + csFieldName = csArray[1]; + mdtFieldName = mdtArray[1]; + + DescribeSObjectResult objDef = Schema.getGlobalDescribe().get(csArray[0]).getDescribe(); + Map fields = objDef.fields.getMap(); + DescribeFieldResult fieldDesc = fields.get(csFieldName).getDescribe(); + srcFieldResultMap.put(csFieldName.toLowerCase(), fieldDesc); + + srcFieldNames.add(csFieldName); + + String selectClause = 'SELECT ' + csArray[1] + ' ,Name '; + String query = selectClause + ' FROM ' + csArray[0] + ' LIMIT 50000'; + List recordList = Database.query(query); + + mappingInfo.setSrcFieldNames(srcFieldNames); + mappingInfo.setRecordList(recordList); + mappingInfo.setSrcFieldResultMap(srcFieldResultMap); + + } + catch (Exception e) { + System.debug('MetadataMapperSimple.Error Message=' + e.getMessage()); + throw e; + } + } + + // TODO: Currently, this is not implemented (well defaulted to true), but I think + // we should implement to validate the fields that are not supported by Custom Metadata Types. + + /** + * Validate the fields between source and target object. + * e.g. If source Custom Object is having a field of type 'masterdetail', + * then we should flag it an error or warning? + * + * */ + public override boolean validate(){ + return true; + } + + public override void mapSourceTarget() { + Map csToMDT_fieldMapping = mappingInfo.getCSToMDT_fieldMapping(); + csToMDT_fieldMapping.put(csFieldName, mdtFieldName); + } + +} diff --git a/custom_md_loader/classes/MetadataMapperSimple.cls-meta.xml b/custom_md_loader/classes/MetadataMapperSimple.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperSimple.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataMapperType.cls b/custom_md_loader/classes/MetadataMapperType.cls new file mode 100644 index 0000000..b277f5f --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperType.cls @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public enum MetadataMapperType { ASIS, SIMPLE, CUSTOM } diff --git a/custom_md_loader/classes/MetadataMapperType.cls-meta.xml b/custom_md_loader/classes/MetadataMapperType.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataMapperType.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataMappingInfo.cls b/custom_md_loader/classes/MetadataMappingInfo.cls new file mode 100644 index 0000000..1056457 --- /dev/null +++ b/custom_md_loader/classes/MetadataMappingInfo.cls @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public with sharing class MetadataMappingInfo { + + private final Set standardFields = new Set(); + private String customSettingName; + private String customMetadadataTypeName; + + private List srcFieldNames; + private List recordList; + private Map srcFieldResultMap; + + private Map csToMDT_fieldMapping = new Map(); + + public MetadataMappingInfo() { + standardFields.add(AppConstants.DEV_NAME_ATTRIBUTE); + standardFields.add(AppConstants.LABEL_ATTRIBUTE); + standardFields.add(AppConstants.DESC_ATTRIBUTE); + } + + public Set getStandardFields() { + return standardFields; + } + + public List getSrcFieldNames() { + return srcFieldNames; + } + + public List getRecordList() { + return recordList; + } + + public void setSrcFieldNames(List names) { + this.srcFieldNames = names; + } + + public void setRecordList(List records) { + this.recordList = records; + } + + public Map getCSToMDT_fieldMapping() { + return this.csToMDT_fieldMapping; + } + + public void setCSToMDT_fieldMapping(Map csToMDT_fieldMapping) { + this.csToMDT_fieldMapping = csToMDT_fieldMapping; + } + + public String getCustomSettingName() { + return this.customSettingName; + } + + public void setCustomSettingName(String customSettingName) { + this.customSettingName = customSettingName; + } + + public String getCustomMetadadataTypeName() { + return this.customMetadadataTypeName; + } + + public void setCustomMetadadataTypeName(String customMetadadataTypeName) { + this.customMetadadataTypeName = customMetadadataTypeName; + } + + public Map getSrcFieldResultMap() { + return this.srcFieldResultMap; + } + + public void setSrcFieldResultMap(Map fieldResult) { + this.srcFieldResultMap = fieldResult; + } + +} diff --git a/custom_md_loader/classes/MetadataMappingInfo.cls-meta.xml b/custom_md_loader/classes/MetadataMappingInfo.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataMappingInfo.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataMigrationController.cls b/custom_md_loader/classes/MetadataMigrationController.cls new file mode 100644 index 0000000..a79bfdf --- /dev/null +++ b/custom_md_loader/classes/MetadataMigrationController.cls @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public class MetadataMigrationController { + static MetadataService.MetadataPort service = MetadataUtil.getPort(); + + private final Set standardFieldsInHeader = new Set(); + private List nonSortedApiNames; + + public boolean showRecordsTable{get;set;} + public String selectedType{get;set;} + public Blob csvFileBody{get;set;} + public SObject[] records{get;set;} + public List cmdTypes{public get;set;} + public String selectedOpTypeAsIs{get;set;} + public String selectedOpTypeSimple{get;set;} + public String selectedOpTypeCustom{get;set;} + public List opTypes{public get;set;} + public List objCreationOpTypes{public get;set;} + + public List fieldNamesForDisplay{public get;set;} + + public String customSettingFromFieldAsIs{public get;set;} + public String customSettingFromFieldSimple{public get;set;} + public String cmdToFieldSimple{public get;set;} + public String customSettingFromFieldJson{public get;set;} + public String cmdToFieldJson{public get;set;} + + public String csFieldObjCreation {public get;set;} + public String cmtFieldObjCreation {public get;set;} + public String opTypeFieldObjCreation {public get;set;} + + public String csNameApexMetadata{public get;set;} + public String cmdNameApexMetadata{public get;set;} + public String jsonMappingApexMetadata{public get;set;} + + public String jsonMapping{public get;set;} + + public boolean asyncDeployInProgress{get;set;} + + public boolean isMessage {get;set;} + + public MetadataOpType opType = MetadataOpType.APEXWRAPPER; + //public MetadataOpType opType = MetadataOpType.METADATAAPEX; + + public MetadataMigrationController() { + + service.timeout_x = 40000; + + isMessage = false; + asyncDeployInProgress = false; + + showRecordsTable = false; + loadCustomMetadataMetadata(); + + //No full name here since we don't want to allow that in the csv header. It is a generated field using type dev name and record dev name/label. + standardFieldsInHeader.add(AppConstants.DEV_NAME_ATTRIBUTE); + standardFieldsInHeader.add(AppConstants.LABEL_ATTRIBUTE); + standardFieldsInHeader.add(AppConstants.DESC_ATTRIBUTE); + + opTypes = new List(); + opTypes.add(new SelectOption(MetadataOpType.APEXWRAPPER.name(), 'Sync Operation')); + opTypes.add(new SelectOption(MetadataOpType.METADATAAPEX.name(), 'Async Operation')); + + objCreationOpTypes = new List(); + objCreationOpTypes.add(new SelectOption(MetadataOpType.APEXWRAPPER.name(), 'Sync Operation')); + + } + + /** + * Queries to find all custom metadata types in the org and make it available to the VF page as drop down + */ + private void loadCustomMetadataMetadata(){ + List entityDefinitions =[select QualifiedApiName from EntityDefinition where IsCustomizable =true]; + for(SObject entityDefinition : entityDefinitions){ + String entityQualifiedApiName = (String)entityDefinition.get(AppConstants.QUALIFIED_API_NAME_ATTRIBUTE); + if(entityQualifiedApiName.endsWith(AppConstants.MDT_SUFFIX)) { + if(cmdTypes == null) { + cmdTypes = new List(); + cmdTypes.add(new SelectOption(AppConstants.SELECT_STRING, AppConstants.SELECT_STRING)); + } + cmdTypes.add(new SelectOption(entityQualifiedApiName, entityQualifiedApiName)); + } + } + } + + private void init(String selectedOpType) { + opType = MetadataOpType.APEXWRAPPER; + if(selectedOpType == MetadataOpType.METADATAAPEX.name()) { + opType = MetadataOpType.METADATAAPEX; + } + } + + public PageReference migrateAsIsWithObjCreation() { + init(opTypeFieldObjCreation); + + MetadataLoader loader = MetadataLoaderFactory.getLoader(opType); + loader.migrateAsIsWithObjCreation(csFieldObjCreation, cmtFieldObjCreation); + + MetadataResponse response = loader.getMetadataResponse(); + + if(response.isSuccess()) { + List messages = response.getMessages(); + for(MetadataResponse.Message message: messages) { + ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.INFO, message.messageDetail); + ApexPages.addMessage(msg); + } + isMessage = true; + } + else { + List messages = response.getMessages(); + for(MetadataResponse.Message message: messages) { + ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.ERROR, message.messageDetail); + ApexPages.addMessage(msg); + } + isMessage = true; + } + + return null; + } + + public PageReference migrateAsIsMapping() { + init(selectedOpTypeAsIs); + + MetadataLoader loader = MetadataLoaderFactory.getLoader(opType); + loader.migrateAsIsMapping(customSettingFromFieldAsIs, selectedType); + MetadataResponse response = loader.getMetadataResponse(); + + if(response.isSuccess()) { + List messages = response.getMessages(); + for(MetadataResponse.Message message: messages) { + ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.INFO, message.messageDetail); + ApexPages.addMessage(msg); + } + isMessage = true; + } + else { + List messages = response.getMessages(); + for(MetadataResponse.Message message: messages) { + ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.ERROR, message.messageDetail); + ApexPages.addMessage(msg); + } + isMessage = true; + } + + return null; + } + + public PageReference migrateSimpleMapping() { + init(selectedOpTypeSimple); + MetadataLoader loader = MetadataLoaderFactory.getLoader(opType); + loader.migrateSimpleMapping(customSettingFromFieldSimple, cmdToFieldSimple); + MetadataResponse response = loader.getMetadataResponse(); + if(response.isSuccess()) { + List messages = response.getMessages(); + for(MetadataResponse.Message message: messages) { + ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.INFO, message.messageDetail); + ApexPages.addMessage(msg); + } + isMessage = true; + } + else{ + List messages = response.getMessages(); + for(MetadataResponse.Message message: messages) { + ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.ERROR, message.messageDetail); + ApexPages.addMessage(msg); + } + isMessage = true; + } + + return null; + } + + public PageReference migrateCustomMapping() { + init(selectedOpTypeCustom); + MetadataLoader loader = MetadataLoaderFactory.getLoader(opType); + loader.migrateCustomMapping(customSettingFromFieldJson, cmdToFieldJson, jsonMapping); + MetadataResponse response = loader.getMetadataResponse(); + List messages = response.getMessages(); + + if(response.isSuccess()) { + for(MetadataResponse.Message message: messages) { + ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.INFO, message.messageDetail); + ApexPages.addMessage(msg); + } + isMessage = true; + } + else { + for(MetadataResponse.Message message: messages) { + ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.ERROR, message.messageDetail); + ApexPages.addMessage(msg); + } + isMessage = true; + } + + return null; + } + +} diff --git a/custom_md_loader/classes/MetadataMigrationController.cls-meta.xml b/custom_md_loader/classes/MetadataMigrationController.cls-meta.xml new file mode 100644 index 0000000..add07b2 --- /dev/null +++ b/custom_md_loader/classes/MetadataMigrationController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 34.0 + Active + diff --git a/custom_md_loader/classes/MetadataMigrationException.cls b/custom_md_loader/classes/MetadataMigrationException.cls new file mode 100644 index 0000000..e68cac4 --- /dev/null +++ b/custom_md_loader/classes/MetadataMigrationException.cls @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public class MetadataMigrationException extends Exception{} diff --git a/custom_md_loader/classes/MetadataMigrationException.cls-meta.xml b/custom_md_loader/classes/MetadataMigrationException.cls-meta.xml new file mode 100644 index 0000000..1a6c043 --- /dev/null +++ b/custom_md_loader/classes/MetadataMigrationException.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/custom_md_loader/classes/MetadataObjectCreator.cls b/custom_md_loader/classes/MetadataObjectCreator.cls new file mode 100644 index 0000000..3b7114b --- /dev/null +++ b/custom_md_loader/classes/MetadataObjectCreator.cls @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +public with sharing class MetadataObjectCreator { + + private static MetadataService.MetadataPort service = MetadataUtil.getPort(); + // Supported data types during migration. If source custom setting or + // source custom object has extra data types than below map, it will + // not work. You can always use custom mapping approach in that case. + public static final Map displayTypeToTypeMap = + new Map{ + DisplayType.Boolean => 'Checkbox', + DisplayType.Date => 'Date', + DisplayType.DateTime => 'DateTime', + DisplayType.Double => 'Number', + DisplayType.Email => 'Email', + DisplayType.EncryptedString => 'Text', + DisplayType.Integer => 'Number', + DisplayType.Percent => 'Number', + DisplayType.Phone => 'Phone', + DisplayType.Picklist => 'Picklist', + DisplayType.String => 'Text', + DisplayType.TextArea => 'TextArea', + DisplayType.URL => 'Url'}; + + public static void createCustomObject(MetadataMappingInfo mappingInfo) { + try { + String fullName = mappingInfo.getCustomMetadadataTypeName(); + + String strippedLabel = fullName.replaceAll('\\W+', '_').replaceAll('__+', '_').replaceAll('\\A[^a-zA-Z]+', '').replaceAll('_$', ''); + + String pluralLabel = fullName.subString(0, fullName.indexOf(AppConstants.MDT_SUFFIX)); + String label = pluralLabel; + pluralLabel = pluralLabel + 's'; + + MetadataService.CustomObject customObject = new MetadataService.CustomObject(); + customObject.fullName = fullName; + customObject.label = label; + customObject.pluralLabel = pluralLabel; + List results = + service.createMetadata( + new MetadataService.Metadata[] { customObject }); + handleSaveResults(results[0]); + + } + catch (Exception e) { + System.debug('createCustomObject.Exception-->' + e.getMessage()); + throw e; + } + } + + public static void createCustomField(MetadataMappingInfo mappingInfo) { + try { + String fullName = mappingInfo.getCustomMetadadataTypeName(); + + String strippedLabel = fullName.replaceAll('\\W+', '_').replaceAll('__+', '_').replaceAll('\\A[^a-zA-Z]+', '').replaceAll('_$', ''); + + String fieldFullName = ''; + String label = ''; + String type_x = ''; + + Map descFieldResultMap = mappingInfo.getSrcFieldResultMap(); + + List customFields = new List(); + integer counter = 0; + for(String csField : mappingInfo.getCSToMDT_fieldMapping().keySet()) { + if(mappingInfo.getCSToMDT_fieldMapping().get(csField).endsWith('__c')){ + + Schema.DescribeFieldResult descFieldResult = descFieldResultMap.get(csField.toLowerCase()); + String cmtField = mappingInfo.getCSToMDT_fieldMapping().get(csField); + fieldFullName = fullName + '.' + cmtField; + label = descFieldResult.getLabel(); + type_x = displayTypeToTypeMap.get(descFieldResult.getType()); + + MetadataService.CustomField customField = new MetadataService.CustomField(); + customFields.add(customField); + customField.fullName = fieldFullName; + customField.label = label; + customField.type_x = type_x; + + // Field datatype specifics + if(type_x == 'Number' || type_x == 'Percent') { + customField.precision = descFieldResult.getPrecision(); + customField.scale = descFieldResult.getScale(); + } + if(type_x == 'Checkbox') { + customField.defaultValue = + descFieldResult.getDefaultValue() == null ? 'false' : String.valueOf(descFieldResult.getDefaultValue()); + } + + boolean lengthReq = true; + if(descFieldResult.getLength() == 0 + || type_x == 'Email' || type_x == 'Phone' + || type_x == 'URL' || type_x == 'Url' + || type_x == 'TextArea' || type_x == 'Picklist') { + lengthReq = false; + } + if(lengthReq && descFieldResult.getLength() != 0 ) { + customField.length = descFieldResult.getLength(); + } + if(type_x == 'Picklist') { + customField.type_x = 'Picklist'; + Metadataservice.Picklist pt = new Metadataservice.Picklist(); + pt.sorted = false; + List picklistValues = new List(); + for(Schema.PicklistEntry entry : descFieldResult.getPicklistValues()) { + Metadataservice.PicklistValue picklistValue = new Metadataservice.PicklistValue(); + picklistValue.fullName = entry.getLabel(); + picklistValue.default_x = entry.isDefaultValue(); + picklistValues.add(picklistValue); + } + pt.picklistValues = picklistValues; + customField.picklist = pt; + } + // process as batches of 10 + if(counter == 9) { + List results = + service.createMetadata(customFields); + handleSaveResults(results[0]); + customFields.clear(); + } + counter++; + } + } + if(customFields.size() > 0) { + List results = + service.createMetadata(customFields); + handleSaveResults(results[0]); + customFields.clear(); + } + } + catch (Exception e) { + System.debug('createCustomField.Exception-->' + e.getMessage()); + throw e; + } + } + + /** + * Example helper method to interpret a SaveResult, throws an exception if errors are found + **/ + private static void handleSaveResults(MetadataService.SaveResult saveResult) { + // Nothing to see? + if(saveResult==null || saveResult.success) + return; + // Construct error message and throw an exception + if(saveResult.errors!=null) { + List messages = new List(); + messages.add( + (saveResult.errors.size()==1 ? 'Error ' : 'Errors ') + + 'occurred processing component ' + saveResult.fullName + '.'); + for(MetadataService.Error error : saveResult.errors) + messages.add( + error.message + ' (' + error.statusCode + ').' + + ( error.fields!=null && error.fields.size()>0 ? + ' Fields ' + String.join(error.fields, ',') + '.' : '' ) ); + if(messages.size()>0) + throw new MetadataMigrationException(String.join(messages, ' ')); + } + if(!saveResult.success) + throw new MetadataMigrationException(Label.ERROR_REQUEST_FAILED_NO_ERROR); + } + +} diff --git a/custom_md_loader/classes/MetadataObjectCreator.cls-meta.xml b/custom_md_loader/classes/MetadataObjectCreator.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataObjectCreator.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataOpType.cls b/custom_md_loader/classes/MetadataOpType.cls new file mode 100644 index 0000000..1953cd2 --- /dev/null +++ b/custom_md_loader/classes/MetadataOpType.cls @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public enum MetadataOpType { APEXWRAPPER, METADATAAPEX } diff --git a/custom_md_loader/classes/MetadataOpType.cls-meta.xml b/custom_md_loader/classes/MetadataOpType.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataOpType.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataResponse.cls b/custom_md_loader/classes/MetadataResponse.cls new file mode 100644 index 0000000..1e527a7 --- /dev/null +++ b/custom_md_loader/classes/MetadataResponse.cls @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public with sharing class MetadataResponse { + + private boolean isSuccess; + private List messages; + private MetadataMappingInfo mappingInfo; + + public MetadataResponse() { + } + + public MetadataResponse(boolean bIsSuccess, MetadataMappingInfo info, List messagesList) { + this.isSuccess = bIsSuccess; + this.messages = messagesList; + this.mappingInfo = info; + } + + public boolean isSuccess() { + return this.isSuccess; + } + + public void setIsSuccess(boolean isSuccess) { + this.isSuccess = isSuccess; + } + + public void setMappingInfo(MetadataMappingInfo info) { + this.mappingInfo = info; + } + public MetadataMappingInfo getMappingInfo() { + return this.mappingInfo; + } + + public List getMessages() { + return this.messages; + } + + public void setMessages(List msg) { + this.messages = msg; + } + + public with sharing class Message { + public Integer messageCode; + public String messageDetail; + + public Message() { + } + + public Message(Integer code, String message) { + this.messageCode = code; + this.messageDetail = message; + } + } + + public String debug() { + return 'MetadataResponse{' + 'success=' + isSuccess() + ', messages=' + getMessages() + ', mapping info=' + getMappingInfo() + '}'; + } +} diff --git a/custom_md_loader/classes/MetadataResponse.cls-meta.xml b/custom_md_loader/classes/MetadataResponse.cls-meta.xml new file mode 100644 index 0000000..e2f92fa --- /dev/null +++ b/custom_md_loader/classes/MetadataResponse.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/custom_md_loader/classes/MetadataService.cls-meta.xml b/custom_md_loader/classes/MetadataService.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/MetadataService.cls-meta.xml +++ b/custom_md_loader/classes/MetadataService.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/MetadataServiceTest.cls-meta.xml b/custom_md_loader/classes/MetadataServiceTest.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/MetadataServiceTest.cls-meta.xml +++ b/custom_md_loader/classes/MetadataServiceTest.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/MetadataUtil.cls b/custom_md_loader/classes/MetadataUtil.cls index 94ba939..a1e6650 100644 --- a/custom_md_loader/classes/MetadataUtil.cls +++ b/custom_md_loader/classes/MetadataUtil.cls @@ -30,7 +30,7 @@ public class MetadataUtil { allCmdProps = service.listMetadata(queries, 34); mdApiStatus = Status.AVAILABLE; } catch (CalloutException e) { - if (!e.getMessage().contains('Unauthorized endpoint, please check Setup')) { + if (!e.getMessage().contains(AppConstants.ERROR_UNAUTHORIZED_ENDPOINT)) { throw e; } mdApiStatus = Status.UNAVAILABLE; @@ -62,18 +62,18 @@ public class MetadataUtil { MetadataService.Metadata[] customMetadataRecords = new MetadataService.Metadata[fieldValues.size()]; // separated out columns as they were coming as string like: "DeveloperName;Label;Description;" // it would pass the conditions under isHeadervalid() - Set fieldNameSet; + Set fieldNameSet = new Set(); for (String fieldNames :header) { if (String.isBlank(fieldNames)) { continue; } - fieldNameSet = new Set(fieldNames.split(';')); + fieldNameSet.addAll(fieldNames.split(';')); } for(List singleRowOfValues : fieldValues) { if(header != null && header.size() != singleRowOfValues.size()) { - ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, AppConstants.INVALID_FILE_ROW_SIZE_DOESNT_MATCH + + ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, System.Label.INVALID_FILE_ROW_SIZE_DOESNT_MATCH + (startIndex + rowCount)); ApexPages.addMessage(errorMessage); return; @@ -84,14 +84,14 @@ public class MetadataUtil { Map fieldsAndValues = new Map(); // separated out field values as they were coming as string like: "XXX;YYY;ZZZZ;" - List fieldValueList; + List fieldValueList = new List(); for (String singleRowFieldValues :singleRowOfValues) { if (String.isBlank(singleRowFieldValues)) { continue; } - fieldValueList = new List(singleRowFieldValues.split(';')); + fieldValueList.addAll(singleRowFieldValues.split(';')); } for(String fieldName : fieldNameSet) { if(fieldName.equals(AppConstants.DEV_NAME_ATTRIBUTE)) { diff --git a/custom_md_loader/classes/MetadataUtil.cls-meta.xml b/custom_md_loader/classes/MetadataUtil.cls-meta.xml index 9aeda45..add07b2 100644 --- a/custom_md_loader/classes/MetadataUtil.cls-meta.xml +++ b/custom_md_loader/classes/MetadataUtil.cls-meta.xml @@ -1,5 +1,5 @@ - 34.0 - Active + 34.0 + Active diff --git a/custom_md_loader/classes/MetadataWrapperApiLoader.cls b/custom_md_loader/classes/MetadataWrapperApiLoader.cls new file mode 100644 index 0000000..b79ac23 --- /dev/null +++ b/custom_md_loader/classes/MetadataWrapperApiLoader.cls @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +public with sharing class MetadataWrapperApiLoader extends MetadataLoader { + private static MetadataService.MetadataPort port; + + public static MetadataService.MetadataPort getPort() { + if (port == null) { + port = new MetadataService.MetadataPort(); + port.sessionHeader = new MetadataService.SessionHeader_element(); + port.sessionHeader.sessionId = UserInfo.getSessionId(); + } + return port; + } + + public override void migrateAsIsWithObjCreation(String csName, String cmtName) { + try{ + super.migrateAsIsWithObjCreation(csName, cmtName); + MetadataMappingInfo mappingInfo = getMapper().getMappingInfo(); + + if(!response.isSuccess()) { + throw new MetadataMigrationException(Label.ERROR_OP_FAILED); + return; + } + MetadataObjectCreator.createCustomObject(mappingInfo); + MetadataObjectCreator.createCustomField(mappingInfo); + + migrate(mappingInfo); + } + catch (Exception e) { + List messages = response.getMessages(); + if(messages == null) { + messages = new List(); + } + messages.add(new MetadataResponse.Message(300, e.getMessage())); + response.setIsSuccess(false); + response.setMessages(messages); + + return; + } + buildResponse(); + } + + public override void migrateAsIsMapping(String csName, String cmtName) { + super.migrateAsIsMapping(csName, cmtName); + buildResponse(); + } + + public override void migrateSimpleMapping(String csNameWithField, String cmtNameWithField) { + super.migrateSimpleMapping(csNameWithField, cmtNameWithField); + buildResponse(); + } + + public override void migrateCustomMapping(String csName, String cmtName, String mapping) { + super.migrateCustomMapping(csName, cmtName, mapping); + buildResponse(); + } + + private void buildResponse() { + if(response.IsSuccess()) { + List messages = new List(); + messages.add(new MetadataResponse.Message(100, Label.MSG_MIGRATION_COMPLETED)); + response.setIsSuccess(true); + response.setMessages(messages); + } + } + + public override void migrate(MetadataMappingInfo mappingInfo) { + System.debug('MetadataWrapperApiLoader.migrate -->'); + + try { + String devName; + String label; + Integer rowCount = 0; + + String cmdName = mappingInfo.getCustomMetadadataTypeName(); + Map descFieldResultMap = mappingInfo.getSrcFieldResultMap(); + Map srcTgtFieldsMap = mappingInfo.getCSToMDT_fieldMapping(); + + MetadataService.Metadata[] customMetadataRecords = new MetadataService.Metadata[mappingInfo.getRecordList().size()]; + Map fieldsAndValues = new Map(); + + for(sObject csRecord : mappingInfo.getRecordList()) { + + String typeDevName = cmdName.subString(0, cmdName.indexOf(AppConstants.MDT_SUFFIX)); + System.debug('typeDevName ->' + typeDevName); + + for(String csField : srcTgtFieldsMap.keySet()) { + // Set Target, Source + Schema.DescribeFieldResult descCSFieldResult = descFieldResultMap.get(csField.toLowerCase()); + + if(descCSFieldResult.getType().name() == 'DATETIME') { + + if(csRecord.get(csField) != null) { + Datetime dt = DateTime.valueOf(csRecord.get(csField)); + // TODO: Fetch date format from user pref? + String formattedDateTime = dt.format('yyyy-MM-dd\'T\'HH:mm:ss.SSSZ'); + fieldsAndValues.put(srcTgtFieldsMap.get(csField), formattedDateTime); + } + } + else { + fieldsAndValues.put(srcTgtFieldsMap.get(csField), String.valueOf(csRecord.get(csField))); + } + } + + if(csRecord.get(AppConstants.CS_NAME_ATTRIBUTE) != null) { + fieldsAndValues.put(AppConstants.FULL_NAME_ATTRIBUTE, typeDevName + '.'+ (String)csRecord.get(AppConstants.CS_NAME_ATTRIBUTE) ); + fieldsAndValues.put(AppConstants.FULL_NAME_ATTRIBUTE, (String)csRecord.get(AppConstants.CS_NAME_ATTRIBUTE) ); + fieldsAndValues.put(AppConstants.LABEL_ATTRIBUTE, (String)csRecord.get(AppConstants.CS_NAME_ATTRIBUTE) ); + + String strippedLabel = (String)csRecord.get(AppConstants.CS_NAME_ATTRIBUTE); + String tempVal = strippedLabel.substring(0, 1); + + if(tempVal.isNumeric() || tempVal == '-') { + strippedLabel = 'X' + strippedLabel; + } + + System.debug('strippedLabel -> 1 *' + strippedLabel); + strippedLabel = strippedLabel.replaceAll('\\W+', '_').replaceAll('__+', '_').replaceAll('\\A[^a-zA-Z]+', '').replaceAll('_$', ''); + System.debug('strippedLabel -> 2 *' + strippedLabel); + + //default fullName to type_dev_name.label + fieldsAndValues.put(AppConstants.FULL_NAME_ATTRIBUTE, typeDevName + '.'+ strippedLabel); + + System.debug(AppConstants.FULL_NAME_ATTRIBUTE + ' ::: ' + typeDevName + '.' + strippedLabel); + } + customMetadataRecords[rowCount++] = transformToCustomMetadata(mappingInfo.getStandardFields(), fieldsAndValues); + } + upsertMetadataAndValidate(customMetadataRecords); + + } + catch (Exception e) { + System.debug('MetadataWrapperApiLoader.Error Message=' + e.getMessage()); + List messages = new List(); + messages.add(new MetadataResponse.Message(100, e.getMessage())); + + response.setIsSuccess(false); + response.setMessages(messages); + + } + } + + /* + * Transformation utility to turn the configuration values into custom metadata values + * This method to modify Metadata is only approved for Custom Metadata Records. Note that the number of custom metadata + * values which can be passed in one update has been increased to 200 values (just for custom metadata) + * We recommend to create new type if more fields are needed. + * Using https://github.com/financialforcedev/apex-mdapi + */ + private MetadataService.CustomMetadata transformToCustomMetadata(Set standardFields, Map fieldsAndValues){ + MetadataService.CustomMetadata customMetadata = new MetadataService.CustomMetadata(); + customMetadata.label = fieldsAndValues.get(AppConstants.LABEL_ATTRIBUTE); + customMetadata.fullName = fieldsAndValues.get(AppConstants.FULL_NAME_ATTRIBUTE); + customMetadata.description = fieldsAndValues.get(AppConstants.DESC_ATTRIBUTE); + + //custom fields + MetadataService.CustomMetadataValue[] customMetadataValues = new List(); + if(fieldsAndValues != null) { + for (String fieldName : fieldsAndValues.keySet()) { + if(!standardFields.contains(fieldName) && !AppConstants.FULL_NAME_ATTRIBUTE.equals(fieldName)){ + MetadataService.CustomMetadataValue cmRecordValue = new MetadataService.CustomMetadataValue(); + cmRecordValue.field=fieldName; + cmRecordValue.value= fieldsAndValues.get(fieldName); + customMetadataValues.add(cmRecordValue); + } + } + } + customMetadata.values = customMetadataValues; + return customMetadata; + } + + private void upsertMetadataAndValidate(MetadataService.Metadata[] records) { + List results = getPort().upsertMetadata(records); + if(results!=null) { + for(MetadataService.UpsertResult upsertResult : results) { + if(upsertResult==null || upsertResult.success) { + continue; + } + // Construct error message and throw an exception + if(upsertResult.errors!=null){ + List messages = new List(); + messages.add( + (upsertResult.errors.size()==1 ? 'Error ' : 'Errors ') + + 'occured processing component ' + upsertResult.fullName + '.'); + for(MetadataService.Error error : upsertResult.errors){ + messages.add(error.message + ' (' + error.statusCode + ').' + + ( error.fields!=null && error.fields.size()>0 ? + ' Fields ' + String.join(error.fields, ',') + '.' : '' ) ); + } + if(messages.size()>0) { + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, String.join(messages, ' '))); + return; + } + } + if(!upsertResult.success) { + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, Label.ERROR_REQUEST_FAILED_NO_ERROR)); + return; + } + } + } + } + +} diff --git a/custom_md_loader/classes/MetadataWrapperApiLoader.cls-meta.xml b/custom_md_loader/classes/MetadataWrapperApiLoader.cls-meta.xml new file mode 100644 index 0000000..add07b2 --- /dev/null +++ b/custom_md_loader/classes/MetadataWrapperApiLoader.cls-meta.xml @@ -0,0 +1,5 @@ + + + 34.0 + Active + diff --git a/custom_md_loader/labels/CustomLabels.labels b/custom_md_loader/labels/CustomLabels.labels new file mode 100644 index 0000000..9e6990d --- /dev/null +++ b/custom_md_loader/labels/CustomLabels.labels @@ -0,0 +1,179 @@ + + + + ERROR_JSON_BAD_FORMAT + CMT + en_US + false + The provided Json String was badly formatted. + The provided Json String was badly formatted. + + + ERROR_JSON_EMPTY + CMT + en_US + false + No field values were found in the Json String. + No field values were found in the Json String. + + + ERROR_REQUEST_FAILED_NO_ERROR + CMT + en_US + false + Request failed with no specified error. + Request failed with no specified error. + + + ERROR_OP_FAILED + CMT + en_US + false + Operation failed, please check messages! + Operation failed, please check messages! + + + ERROR_UNAUTHORIZED_ENDPOINT + CMT + en_US + false + Unauthorized endpoint, please check Setup + Unauthorized endpoint, please check Setup + + + MSG_CUSTOM_SETTINGS_EXISTS + CMT + en_US + false + Make sure Custom Setting exists in the org! + Make sure Custom Setting exists in the org! + + + MSG_CS_MDT_REQ + CMT + en_US + false + Custom Setting Api Name and Custom Metadata Types Api names are required! + Custom Setting Api Name and Custom Metadata Types Api names are required! + + + MSG_CS_MDT_EXISTS + CMT + en_US + false + Make sure Custom Setting and Custom Metadata Types exists in the org! + Make sure Custom Setting and Custom Metadata Types exists in the org! + + + MSG_CHECK_API_NAMES + CMT + en_US + false + Please check the Api names! + Please check the Api names! + + + MSG_CS_MDT_FIELD_NAME_REQ + CMT + en_US + false + Custom Setting Name/Field Name and CMT/Field Name names are required! + Custom Setting Name/Field Name and CMT/Field Name names are required! + + + MSG_CS_MDT_FIELD_NAME_FORMAT + CMT + en_US + false + Please check the format, Custom Setting.field and Custom Metadata Types.field + Please check the format, Custom Setting.field and Custom Metadata Types.field + + + MSG_CS_MDT_MAPPING_REQ + CMT + en_US + false + Custom Setting Name, Custom Metadata Types and Json Mapping names are required! + Custom Setting Name, Custom Metadata Types and Json Mapping names are required! + + + MSG_JSON_FORMAT + CMT + en_US + false + Please check the Json format! + Please check the Json format! + + + MSG_JSON_API_NAMES + CMT + en_US + false + Please check the Api names in Json! + Please check the Api names in Json! + + + MSG_NOT_SUPPORTED + CMT + en_US + false + Not Supported!!! + Not Supported!!! + + + MSG_MIGRATION_COMPLETED + CMT + en_US + false + Migration Completed! + Migration Completed! + + + MSG_MIGRATION_IN_PROGRESS + CMT + en_US + false + Migration In Progress, check status under Deploy -> Deployment Status, Job Id: + Migration In Progress, check status under Deploy -> Deployment Status, Job Id: + + + MSG_MDT_END + CMT + en_US + false + Custom Metadata Types name should end with + Custom Metadata Types name should end with + + + FILE_MISSING + CMT + en_US + false + Please provide a comma separated file. + Please provide a comma separated file. + + + EMPTY_FILE + CMT + en_US + false + CSV file is empty. + CSV file is empty. + + + TYPE_OPTION_NOT_SELECTED + CMT + en_US + false + Please choose a valid custom metadata type. + Please choose a valid custom metadata type. + + + INVALID_FILE_ROW_SIZE_DOESNT_MATCH + CMT + en_US + false + The number of field values does not match the number of header fields on line + The number of field values does not match the number of header fields on line + + diff --git a/custom_md_loader/package.xml b/custom_md_loader/package.xml index 5470b27..7541006 100755 --- a/custom_md_loader/package.xml +++ b/custom_md_loader/package.xml @@ -1,43 +1,67 @@ - - AppConstants - CSVFileUtil - CustomMetadataLoaderController - CustomMetadataLoaderControllerTest - CustomMetadataUploadController - CustomMetadataUploadControllerTest - MDWrapperWebServiceMock - MetadataService - MetadataServiceTest - MetadataUtil - ApexClass - - - CustomMetadataLoader - CustomMetadataRecordUploader - ApexPage - - - Custom_Metadata_Loader - CustomApplication - - - CountryMapping__mdt.CountryCode__c - CountryMapping__mdt.CountryName__c - CustomField - - - CountryMapping__mdt - CustomObject - - - Custom_Metadata_Loader - CustomTab - - - Custom_Metadata_Loader - PermissionSet - - 34.0 + + AppConstants + CSVFileUtil + CustomMetadataLoaderController + CustomMetadataLoaderControllerTest + CustomMetadataUploadController + CustomMetadataUploadControllerTest + MDWrapperWebServiceMock + MetadataService + MetadataServiceTest + MetadataUtil + MetadataLoader + MetadataMapper + MetadataMapperDefault + MetadataMapperCustom + MetadataMapperSimple + MetadataMappingInfo + MetadataMapperFactory + MetadataMapperType + MetadataLoaderClient + MetadataWrapperApiLoader + MetadataApexApiLoader + MetadataOpType + MetadataLoaderFactory + MetadataResponse + MetadataMigrationController + MetadataObjectCreator + MetadataMigrationException + JsonUtilities + ApexClass + + + CustomMetadataLoader + CustomMetadataRecordUploader + CMTMigrator + ApexPage + + + * + CustomLabels + + + Custom_Metadata_Loader + CustomApplication + + + CountryMapping__mdt.CountryCode__c + CountryMapping__mdt.CountryName__c + CustomField + + + CountryMapping__mdt + CustomObject + + + Custom_Metadata_Loader + CMT_Migrator + CustomTab + + + Custom_Metadata_Loader + PermissionSet + + 40.0 diff --git a/custom_md_loader/pages/CMTMigrator.page b/custom_md_loader/pages/CMTMigrator.page new file mode 100644 index 0000000..0514930 --- /dev/null +++ b/custom_md_loader/pages/CMTMigrator.page @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
Custom Setting/Custom Object Name (From)
Custom Metadata Type Name (To)
Operation Type + + +
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + +
Custom Setting/Custom Object Name (From)
Custom Metadata Type Name (To) + + +
Operation Type + + +
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + +
Custom Setting/Custom Object, Format: CS.fieldName
Custom Metadata Type, Format: CMT.fieldName
Operation Type + + +
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + +
Custom Setting/Custom Object Name
Custom Metadata Type Name
Json Mapping
Operation Type + + +
+ +
+
+ +
+
+
+ +
+ + +
\ No newline at end of file diff --git a/custom_md_loader/pages/CMTMigrator.page-meta.xml b/custom_md_loader/pages/CMTMigrator.page-meta.xml new file mode 100644 index 0000000..49671cd --- /dev/null +++ b/custom_md_loader/pages/CMTMigrator.page-meta.xml @@ -0,0 +1,7 @@ + + + 40.0 + false + false + + diff --git a/custom_md_loader/pages/CustomMetadataLoader.page b/custom_md_loader/pages/CustomMetadataLoader.page index 3474b63..29e3324 100644 --- a/custom_md_loader/pages/CustomMetadataLoader.page +++ b/custom_md_loader/pages/CustomMetadataLoader.page @@ -35,7 +35,7 @@ '' + '' + ''; - binding.open('POST', protocol + '//{!host}/services/Soap/m/34.0'); + binding.open('POST', protocol + '//{!host}/services/Soap/m/40.0'); binding.setRequestHeader('SOAPAction','""'); binding.setRequestHeader('Content-Type', 'text/xml'); binding.onreadystatechange = diff --git a/custom_md_loader/pages/CustomMetadataLoader.page-meta.xml b/custom_md_loader/pages/CustomMetadataLoader.page-meta.xml index d816bc4..97b2db1 100644 --- a/custom_md_loader/pages/CustomMetadataLoader.page-meta.xml +++ b/custom_md_loader/pages/CustomMetadataLoader.page-meta.xml @@ -1,9 +1,10 @@ - - + + - 34.0 - false - false - + 34.0 + false + false + diff --git a/custom_md_loader/pages/CustomMetadataRecordUploader.page b/custom_md_loader/pages/CustomMetadataRecordUploader.page index 67c50e2..d22a11b 100644 --- a/custom_md_loader/pages/CustomMetadataRecordUploader.page +++ b/custom_md_loader/pages/CustomMetadataRecordUploader.page @@ -8,7 +8,7 @@ - + diff --git a/custom_md_loader/pages/CustomMetadataRecordUploader.page-meta.xml b/custom_md_loader/pages/CustomMetadataRecordUploader.page-meta.xml index e8d870f..4c30c92 100644 --- a/custom_md_loader/pages/CustomMetadataRecordUploader.page-meta.xml +++ b/custom_md_loader/pages/CustomMetadataRecordUploader.page-meta.xml @@ -1,9 +1,10 @@ - - + + - 34.0 - false - false - + 34.0 + false + false + diff --git a/custom_md_loader/permissionsets/Custom_Metadata_Loader.permissionset b/custom_md_loader/permissionsets/Custom_Metadata_Loader.permissionset index d8d0135..4f9ab89 100755 --- a/custom_md_loader/permissionsets/Custom_Metadata_Loader.permissionset +++ b/custom_md_loader/permissionsets/Custom_Metadata_Loader.permissionset @@ -24,6 +24,10 @@ CustomMetadataUploadController true + + CustomMetadataUploadController + true + CustomMetadataUploadControllerTest true @@ -53,8 +57,16 @@ CustomMetadataRecordUploader true + + CMTMigrator + true + Custom_Metadata_Loader Visible + + CMT_Migrator + Visible + diff --git a/custom_md_loader/tabs/CMT_Migrator.tab b/custom_md_loader/tabs/CMT_Migrator.tab new file mode 100644 index 0000000..233acd3 --- /dev/null +++ b/custom_md_loader/tabs/CMT_Migrator.tab @@ -0,0 +1,7 @@ + + + + false + Custom68: Gears + CMTMigrator +