4

This is a spinoff form a question of: Are there any plans to update ToolingAPI.cls for APEX to v32 in order to support Lightning Components?

Here we focus on MetadataAPI (in contrary to the OP which is about Lightning access via ToolingAPI accessed from APEX and Visualforce)

The approach is based on top of the wrapper MetadataService.cls found here:

https://github.com/financialforcedev/apex-mdapi

There has been issues reported by @AndrewFawcett and confirmed by @DougChasman that some functionality may not work as expected.

@Andrew + @Dough : you could provide links or short descriptions of the issue here.

However there is (at least) one use case to show an access pattern: we are using this in CodeFusion as an second option beside ToolingAPI access. I'll provide some infos about that as an answer.

3

We are using the MetadataService.cls together with jszip as a kind of Swiss-Army knife to access almost everthing in CodeFusion.

To make Lightning Components accessible I first updated MetadataService.cls from v29 to v32.

The next step has nothing to do with Lightning but my biggest challenge was that some functions has been deprecated in v31. To avoid a redesign of existing code, I hacked MetadataService.cls v32 and reintroduced the old functions as described here MetadataAPI: Missing MetadataPort.checkStatus(String[] asyncProcessId) in MetadataService.cls v32.0

After that it was quite easy to make small tweaks to the APEX, Visualforce and JavaScript methods to load and save AuraDefinitionBundles. We are not using the CRUD functions. And sorry, the provided code is uncleaned and probably not working out of it's context, but could show the pattern quite well.

The basic approach in APEX goes like this (which actually also works for Lightning Components in it's manifestation as AuraDefinitionBundle). But remember the generic (Swiss-Army-Knife) approach, so you won't find any explicit mentioning of AuraDefinitionBundle in the code. Everything will be controlled with MetaDataType (='AuraDefinitionBundle') and MetaDataItem (='YourLightningComponentName')

Retrieve Metadata

public PageReference retrieveMetadataItem() {
    RetrieveCompleted                   = false;
    DeployCompleted                     = false;
    MetaDataRetrieveZip                 = null;
    MetadataFiles                       = null;
 // Construct unmanaged package with list of desired components to retrieve in zip
    MetadataService.MetadataPort
        service                         = createService();
    MetadataService.RetrieveRequest
        retrieveRequest                 = new MetadataService.RetrieveRequest();
    retrieveRequest.apiVersion          = this.MetadataAPIVersion;
    retrieveRequest.packageNames        = null;
    retrieveRequest.singlePackage       = true;
    retrieveRequest.specificFiles       = null;
    retrieveRequest.unpackaged          = new MetadataService.Package_x();
    retrieveRequest.unpackaged.types    = new List<MetadataService.PackageTypeMembers>();
    MetadataService.PackageTypeMembers
        packageType                     = new MetadataService.PackageTypeMembers();
    packageType.name                    = MetaDataType;
    packageType.members                 = new String[] { MetaDataItem };
    retrieveRequest.unpackaged.types.add(packageType);
    AsyncResultRetrieve                         = service.retrieve(retrieveRequest);
    return null;
}

public PageReference checkAsyncRequestRetrieve()    {
    errors = 0;
 // Check the status of the retrieve request
    MetadataService.MetadataPort
        service                         = createService(); 
    MetadataService.AsyncResult[]
        aysncResults                    = service.checkStatus(new String[] { AsyncResultRetrieve.Id });
    if(aysncResults[0].done) {
        if(aysncResults[0].state == 'Error') {      // async errors:
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Info, aysncResults[0].message));
            AsyncResultRetrieve = null;
        } else {                                    // async no errors:
            MetadataService.RetrieveResult          // Place Base64 encoded zip data onto the page for the JSZip library to handle
                retrieveResult          = service.checkRetrieveStatus(AsyncResultRetrieve.id);
            if(retrieveResult.messages != null) { 
                xt.msg('Error '+retrieveResult.messages[0].problem ,'error');
                errors = 1;
            } else {
                xt.msg('Retrieved '+MetaDataItem,'confirm');
            }
            MetaDataRetrieveZip         = retrieveResult.zipFile;
            MetadataFiles               = new List<MetadataFile>();
            AsyncResultRetrieve         = null;
            Retrieving                  = false;
        }
    } else { 
        //xt.msgp('Retrieving metadata...');
    }
    return null;
}

Deploy Metadata

public PageReference deployZip() {
    xt.msgp( 'MedadataAPI: deploying... <span class="asyncCheckCount"></span>');
    Deploying = true;
 // Deploy zip file posted back from the page action function
    MetadataService.MetadataPort    service = createService();
    MetadataService.DeployOptions   deployOptions = new MetadataService.DeployOptions();
        deployOptions.allowMissingFiles = false;
        deployOptions.autoUpdatePackage = false;
        deployOptions.checkOnly         = false;
        deployOptions.ignoreWarnings    = false;
        deployOptions.performRetrieve   = false;
        deployOptions.purgeOnDelete     = false;
        deployOptions.rollbackOnError   = true;
        deployOptions.runAllTests       = false;
        deployOptions.runTests          = null;
        deployOptions.singlePackage     = true;
    AsyncResultDeploy = service.deploy(ZipData, DeployOptions);
    return null;
}

public PageReference checkAsyncRequestDeploy() {    // Check the status of the retrieve request
    try {
        errors = 0;
        string  successTexts = '';
        MetadataService.MetadataPort
            service                     = createService();
        MetadataService.AsyncResult[]
            aysncResults                = service.checkStatus(new String[] { AsyncResultDeploy.Id });
        if(aysncResults[0].done) {                  // async done
            this.DeployCompleted        = true; // update global completed
            Deploying                   = false;
            if(aysncResults[0].state == 'Error') {  // async errors:
                ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Info, aysncResults[0].message));
                AsyncResultDeploy       = null;
            } else {                                // async no errors:
                xt.msg('Deployment complete');
                MetadataService.DeployResult
                    deployResult            = service.checkDeployStatus(AsyncResultDeploy.Id); // Deployment errors?
                //v29
                if(deployResult.details != null ) {
                    if(deployResult.details.componentFailures != null ){
                        for(MetadataService.DeployMessage deployMessage : deployResult.details.componentFailures) {
                            xt.msg(''
                                + deployMessage.fileName
                                + ' (Line: ' + deployMessage.lineNumber + ': Column:' + deployMessage.columnNumber + ') : '
                                + deployMessage.problem
                            ,'error');
                            errors++;
                        }
                    }
                    if(deployResult.details.componentSuccesses != null ){
                        for(MetadataService.DeployMessage deployMessage : deployResult.details.componentSuccesses) {
                            successTexts += deployMessage.fileName + (deployMessage.id != null ? ''+'<br>'+deployMessage.id : '')+'<br>';
                        }
                    }
                    if(errors==0) {
                        xt.msg(''+successTexts
                        ,'confirm');
                        if(versionManagementIsActive) versionCommit( MetadataItem, MetadataType, OrgNamespacePrefix );
                    }
                }
                AsyncResultDeploy       = null;
            }
        } else {
            xt.msgp( 'MedadataAPI: deploying... <span class="asyncCheckCount"></span>');
        }
    } catch( Exception e ){
        xt.msg(''
            +'catched APEX Error:'+'<br>' + e.getTypeName()+'<br>' + e.getMessage()+'<br>'
            +'in Line: '+e.getLineNumber()+'<br>' + e.getStackTraceString()+'<br>'
        ,'error');
    }
    return null;
}

Zipping

On the Visualforce/Javascript side you have to deal with a zip file, which you have to extract. In it's content, you'll get all your Lightning files as described here: https://www.salesforce.com/us/developer/docs/api_meta/index_Left.htm#CSHID=meta_auradefinitionbundle.htm|StartTopic=Content%2Fmeta_auradefinitionbundle.htm|SkinName=webhelp

File Suffix and Directory Location

A Lightning bundle is a folder that contains definition files. Unlike other metadata components, an AuraDefinitionBundle component isn’t represented by a single component file but instead by a collection of component definition files. Each definition file represents a resource in a bundle, such as markup, applications, code files (including controllers and helpers), events, documentation, and interfaces. For example, this directory structure shows the hierarchy of the folders and files for two bundles: bundle1 and bundle2.

aura
    bundle1
        bundle1.cmp
        bundle1Controller.js
    bundle2
        bundle2.app
        bundle2.cmp
        bundle2Controller.js
        bundle2.auradoc

As an unfortunate there is no zip-support in APEX, so you have to help yourself using JSZip. You can make your changes to the extracted files, update the zip and use the deploy functions to get everything back to Salesforce. But it's slow: it'll take about 5-10 seconds or longer to save... If you want something faster, have a look here: Calling ToolingAPI to save an AuraDefinition using PATCH via JavaScript possible? - this one will save small files in less than 0.5 seconds thanks to Doug Chasmans new approch:

We implemented the Tooling API for Lightning Components backend differently that those others because I refused to go with the de facto slower approach and I hope other meta objects will follow our example :-)

Thumbs up!

5
  • Very nice, +1, are you using the zip components and approach used in the Retrieve and Deploy demos of the Apex Metadata API by any chance? :-) – Andrew Fawcett Dec 5 '14 at 12:23
  • 1
    @AndrewFawcett: your component approach inspired me, but due to maintainability and ease of handling I go with plain JS totally without VF-components. I will opensource the entire CodeFusion project in the near future. On request I can provide links for a beta package of CodeFusion which can read and write Lightning Components with both ToolingAPI and MetadataAPI switchable :-) – Uwe Heim Dec 5 '14 at 12:36
  • @AndrewFawcett btw if Salesforce decides to finally delete checkStatus() in the future and not only to deprecate as it is now, do you have any idea on how to update the code above? – Uwe Heim Dec 5 '14 at 12:42
  • 1
    Here is CodeFusion 2015 BETA 63 login.salesforce.com/packaging/… - to create Lightning components, you still need the Dev Console in this version - but you can edit them. – Uwe Heim Dec 5 '14 at 15:13
  • Still digging on this but have verified that this is no longer an issue in Spring '15 and that I can repro it following AndrewF's instructions on Winter '15. I am actively working to identify what work was done in Spring '15 to make this not an issue and once I have identified the changes I will evaluate the ROI of back porting to Winter '15 – Doug Chasman Dec 5 '14 at 17:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.