afawcett / apex-toolingapi

Apex wrapper for the Salesforce Tooling API
BSD 3-Clause "New" or "Revised" License
134 stars 98 forks source link

INVALID_FIELD_FOR_INSERT_UPDATE : No access to entity "NS__PackagedObject__c" #11

Closed ghost closed 10 years ago

ghost commented 10 years ago

I have a managed (in my ISV package) class that creates an unmanaged class (outside of the package) based on database data.

When I do:

toolingAPI.createSObject(newClass); 

it fails with the error:

INVALID_FIELD_FOR_INSERT_UPDATE : No access to entity 'up2go_2f__deliveryschedule__c'
Error is in expression '{!proceedAction}' in component <apex:commandButton> in component up2go_2f:confirm
Error is in expression '{!doGenerateAndLinkClass}' in component <apex:commandButton> in page up2go_2f:generatehighperformanceclass

The entity it does not seem to have access to is an SObject of a packaged type, so not a class. So I see no reason why someone outside should not be able to use and compile a reference to such a record.

afawcett commented 10 years ago

That is strange, i assume the same user can create the Apex class manually?

ghost commented 10 years ago

Yes and its in a without sharing class

Von meinem iPhone gesendet

Am 04.04.2014 um 18:40 schrieb Andrew Fawcett notifications@github.com:

That is strange, i assume the same user can create the Apex class manually?

— Reply to this email directly or view it on GitHub.

afawcett commented 10 years ago

Have you tried with a simple HelloWorld class? I've submitted a small example just to compare https://github.com/afawcett/apex-toolingapi/blob/master/apex-toolingapi/src/classes/ToolingAPIDemo.cls#L29

ghost commented 10 years ago

A simple class works. Only if I reference packaged object it fails.

Von meinem iPhone gesendet

Am 09.04.2014 um 10:50 schrieb Andrew Fawcett notifications@github.com:

Have you tried with a simple HelloWorld class? I've submitted a small example just to compare https://github.com/afawcett/apex-toolingapi/blob/master/apex-toolingapi/src/classes/ToolingAPIDemo.cls#L29

— Reply to this email directly or view it on GitHub.

afawcett commented 10 years ago

Ok, let me extend me test to this... Thanks.

afawcett commented 10 years ago

This looks like a bug in the Tooling API itself, I've reproduced it Developer Workbench. I'll raise a case, meanwhile I think this can be worked around, by doing what i suspect the Developer Console is doing, notice when you do the same here, it precreates an empty class then updates it, going to try an emulate this in code next.

screen shot 2014-04-09 at 10 38 57

{
    "body": "public class HelloWorld { public void hello() { packagea__WorkOrders__c lookup; } }",
    "name": "HelloWorld",
    "status": "Active",
    "fullName": "HelloWorld",
    "metadata": {
        "status": "Active",
        "packageVersions": [
            {
                "namespace": "packagea",
                "minorNumber": 2,
                "majorNumber": 1
            }
        ],
        "apiVersion": 28
    }
}

This is my test Ape code that generated the above request...

public static void createClass() {
    // Constructs the Tooling API wrapper (default constructor uses user session Id)        
    ToolingAPI toolingAPI = new ToolingAPI();

    ToolingAPI.ApexClass apexClass = new ToolingAPI.ApexClass();
    apexClass.Name = 'HelloWorld';
    apexClass.FullName = 'HelloWorld';
    apexClass.Body = 'public class HelloWorld { public void hello() { packagea__WorkOrders__c lookup; } }';
    apexClass.Status = 'Active';
    apexClass.metadata = new ToolingAPI.ApexClassMetadata();
    apexClass.metadata.apiVersion = 28.0;
    apexClass.metadata.status = 'Active';
    apexClass.metadata.packageVersions = new List<ToolingAPI.PackageVersion>();
    apexClass.metadata.packageVersions.add(new ToolingAPI.PackageVersion());
    apexClass.metadata.packageVersions[0].majorNumber = 1;
    apexClass.metadata.packageVersions[0].minorNumber = 2;
    apexClass.metadata.packageVersions[0].namespace = 'packagea';
    toolingAPI.createSObject(apexClass);
}
afawcett commented 10 years ago

Doing HTTP POST with skeleton class, then HTTP PATCH on the ApexClass object, with a managed field reference also fails (and I also suspect HTTP PATCH on ApexClass is broken generally, see below)....

public static void createAndUpdateClass() {
    // Constructs the Tooling API wrapper (default constructor uses user session Id)        
    ToolingAPI toolingAPI = new ToolingAPI();

    // Create class
    ToolingAPI.ApexClass apexClass = new ToolingAPI.ApexClass();
    apexClass.Name = 'HelloWorld';
    apexClass.FullName = 'HelloWorld';
    apexClass.Body = 'public class HelloWorld { }';
    apexClass.Status = 'Active';
    apexClass.metadata = new ToolingAPI.ApexClassMetadata();
    apexClass.metadata.apiVersion = 28.0;
    apexClass.metadata.status = 'Active';
    apexClass.metadata.packageVersions = new List<ToolingAPI.PackageVersion>();
    apexClass.metadata.packageVersions.add(new ToolingAPI.PackageVersion());
    apexClass.metadata.packageVersions[0].majorNumber = 1;
    apexClass.metadata.packageVersions[0].minorNumber = 2;
    apexClass.metadata.packageVersions[0].namespace = 'packagea';
    ToolingAPI.SaveResult sr = toolingAPI.createSObject(apexClass);

    // Update class
    apexClass = new ToolingAPI.ApexClass();
    apexClass.Id = sr.Id;
    apexClass.Body = 'public class HelloWorld { public void hello() { packagea__WorkOrders__c lookup; } }';
    toolingAPI.updateSObject(apexClass); 
}

Again via Developer Workbench...

screen shot 2014-04-09 at 11 01 32

{
    "body": "public class HelloWorld { public void hello() { packagea__WorkOrders__c lookup; } }"
}

Interestingly.... even without the managed field reference HTTP PATCH fails with the same error...

{
    "body": "public class HelloWorld { public void hello() {  } }"
}

This i assume is another issue with doing HTTP PATCH on an ApexClass, perhaps what is ment in the Salesforce documentation as the following, it just happens to be the same error...

https://www.salesforce.com/us/developer/docs/api/Content/sforce_api_objects_apexclass.htm

Although Apex classes and triggers have the Create and Update field properties, a runtime exception occurs if you try to create or update them using the API. Instead, use the Force.com Migration Tool, the Salesforce user interface, or the Force.com IDE to create or update Apex classes or triggers.

Though clearly we have been able to 'create' via the Tooling API, and the above is from the standard Salesforce API docs, confusing, since in respect to 'create' the behaviour is different depending on Salesforce API and Tooling API. Next thing to try, in respect to updating an Apex class with a managed field reference is via ApexClassMember object, which brings with it quite a lot of overhead and aysnc sadly...

afawcett commented 10 years ago

Ok this rather lengthy process works!

public static void createAndUpdateClass() {
    // Constructs the Tooling API wrapper (default constructor uses user session Id)        
    ToolingAPI tooling = new ToolingAPI();

    // Create class
    ToolingAPI.ApexClass apexClass = new ToolingAPI.ApexClass();
    apexClass.Name = 'HelloWorld';
    apexClass.FullName = 'HelloWorld';
    apexClass.Body = 'public class HelloWorld { }';
    apexClass.Status = 'Active';
    apexClass.metadata = new ToolingAPI.ApexClassMetadata();
    apexClass.metadata.apiVersion = 28.0;
    apexClass.metadata.status = 'Active';
    apexClass.metadata.packageVersions = new List<ToolingAPI.PackageVersion>();
    apexClass.metadata.packageVersions.add(new ToolingAPI.PackageVersion());
    apexClass.metadata.packageVersions[0].majorNumber = 1;
    apexClass.metadata.packageVersions[0].minorNumber = 2;
    apexClass.metadata.packageVersions[0].namespace = 'packagea';
    ToolingAPI.SaveResult sr = tooling.createSObject(apexClass);

    // Delete MetadataContainer             
    List<ToolingAPI.MetadataContainer> containers =  
        (List<ToolingAPI.MetadataContainer>)
            tooling.query(
                'SELECT Id, Name FROM MetadataContainer WHERE Name = \'UpdateHelloWorld\'').records;
    if ( containers != null && ! containers.isEmpty() )
        tooling.deleteSObject(ToolingAPI.SObjectType.MetadataContainer, containers[0].Id);

    // Create MetadataContainer
    ToolingAPI.MetadataContainer container = new ToolingAPI.MetadataContainer();
    container.name = 'UpdateHelloWorld';
    ToolingAPI.SaveResult containerSaveResult = tooling.createSObject(container);
    Id containerId = containerSaveResult.id;

    // Create ApexClassMember and associate them with the MetadataContainer
    ToolingAPI.ApexClassMember apexClassMember = new ToolingAPI.ApexClassMember();      
    apexClassMember.Body = 'public class HelloWorld { public void hello() { packagea__WorkOrders__c lookup; } }';
    apexClassMember.ContentEntityId = sr.id;
    apexClassMember.MetadataContainerId = containerId;      
    ToolingAPI.SaveResult apexClassMemberSaveResult = tooling.createSObject(apexClassMember);

    // Create ContainerAysncRequest to deploy the Apex Classes
    ToolingAPI.ContainerAsyncRequest asyncRequest = new ToolingAPI.ContainerAsyncRequest();     
    asyncRequest.metadataContainerId = containerId;
    asyncRequest.IsCheckOnly = false;       
    ToolingAPI.SaveResult asyncRequestSaveResult = tooling.createSObject(asyncRequest);     

    // The above starts an async background compile, the following needs repeated (polled) to confirm compilation
    ToolingApi.ContainerAsyncRequest containerAsyncRequest = 
        ((List<ToolingAPI.ContainerAsyncRequest>)
            tooling.query(
                'SELECT Id, State, MetadataContainerId, CompilerErrors ' + 
                'FROM ContainerAsyncRequest ' + 
                'WHERE Id = \'' + asyncRequestSaveResult.Id + '\'').records)[0];
    System.debug('State is ' + containerAsyncRequest.State);                
}

This outputs...

11:25:23.187 (2187009136)|USER_DEBUG|[83]|DEBUG|State is Queued

Note that at the end here, one really needs to poll the status (via apex:actionPoller or Batch Apex as per Apex Metadata API approach). ContainerAsyncRequest object to determine the result of the compilation. I just waited a while and checked manually the contents of the class and sure enough the managed field reference was present, thus confirming the above does work.

ghost commented 10 years ago

Is this really important for creating a class?

    apexClass.metadata = new ToolingAPI.ApexClassMetadata();
    apexClass.metadata.apiVersion = 28.0;
    apexClass.metadata.status = 'Active';
    apexClass.metadata.packageVersions = new List<ToolingAPI.PackageVersion>();
    apexClass.metadata.packageVersions.add(new ToolingAPI.PackageVersion());
    apexClass.metadata.packageVersions[0].majorNumber = 1;
    apexClass.metadata.packageVersions[0].minorNumber = 2;
    apexClass.metadata.packageVersions[0].namespace = 'packagea';
ghost commented 10 years ago

Please keep me (on this issue) posted when Salesforce.com replied to you case.

As I understand you answers (thanks a lot) there is no such thing as create and populate a class by clicking a button as it always involves batching/waiting/polling? :-(

What do you mean by using apex:poller? It sound like a visualforce only solution without batch. I would prefer that as this class generation is called by a custom button from a page.

afawcett commented 10 years ago

FYI, I have updated the repo with a more generic example, https://github.com/afawcett/apex-toolingapi/blob/master/apex-toolingapi/src/classes/ToolingAPIDemo.cls#L29

afawcett commented 10 years ago

Sorry, ment to say apex:actionPoller, and yes that is correct i see no option currently that does not involve batching/waiting/polling as you say.

You could still use apex:actionPoller from your custom button apex page, just make the calls to perform the code in the 'action' method on page load (or from confirmation button on page), then retain in view state the async id and use apex:actionPoller to check for the status. Check out my blog on Apex Metadata API for how this is done in that context its the same flow.

afawcett commented 10 years ago

And no, the metadata stuff was only me trying to see if it made a difference, the general example i have committed does not have this. :+1:

afawcett commented 10 years ago

If, in the meantime, if you are looking for a Metadata API approach via a Custom Button check this out, https://github.com/afawcett/declarative-lookup-rollup-summaries/blob/master/rolluptool/src/classes/RollupController.cls

https://github.com/afawcett/declarative-lookup-rollup-summaries/blob/master/rolluptool/src/pages/managetrigger.page

afawcett commented 10 years ago

Salesforce Support Case 10418097 raised.

afawcett commented 10 years ago

The case has been escalated to Salesforce R&D.

afawcett commented 10 years ago

I'll close this issue, since when/if Salesforce fix this i believe the library will just start supporting this also.

afawcett commented 10 years ago

A similar issue has been found and an Idea raised here, https://success.salesforce.com/ideaView?id=08730000000l8a5AAA&sort=2