Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add createHdr() method which allows a header to be passed to the Salesforce create API. #27

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

DonLewisFSI
Copy link

The new createHdr() method allows Saleforce records to be created using one of the Salesforce specialized headers.

Example:
To create a new Lead record using the the default assignment rule:

  my $autoassign = SOAP::Header->name('useDefaultRule' => 'true' );
  my $assign_hdr = SOAP::Header->name('AssignmentRuleHeader' => \$autoassign );
  my $id = $sfdc->createHdr($assign_hdr, %$obj);

@dclendenan
Copy link
Contributor

@DonLewisFSI would it perhaps be more clear if the name was changed? s/createHdr/createWithHeader/ ? createHdr() makes me think that it's generating a header object.

@genio do you have any other concerns with this PR? It seems relatively straightforward and simplifies an increasingly-common use case.

FWIW I work for Salesforce, we use WWW::Salesforce extensively in our internal tools (and have pulled this patch into our internal repo for now).

@genio
Copy link
Member

genio commented Nov 4, 2019

I do not have concern with the idea itself, though I'd like to propose stealing a bit from your added new method and altering the current create method something akin to:

=head2 create

    # create a new account with a hash
    my $res = $sforce->create(type => 'Account', name => 'Foo');
    say $res->envelope->{Body}->{createResponse}->{result}->{success};

    # create a new account with a hashref
    my $res = $sforce->create({type => 'Account', name => 'Foo'});
    say $res->envelope->{Body}->{createResponse}->{result}->{success};

    # create a new account with an extra header and a hash
    my $autoassign = SOAP::Header->name('useDefaultRule' => 'true' );
    my $assign_hdr = SOAP::Header->name('AssignmentRuleHeader' => \$autoassign );
    my $res = $sforce->create($assign_hdr, type => 'Account', name => 'Foo');
    say $res->envelope->{Body}->{createResponse}->{result}->{success};

    # create a new account with an extra header and a hashref
    my $autoassign = SOAP::Header->name('useDefaultRule' => 'true' );
    my $assign_hdr = SOAP::Header->name('AssignmentRuleHeader' => \$autoassign );
    my $res = $sforce->create($assign_hdr, {type => 'Account', name => 'Foo'});
    say $res->envelope->{Body}->{createResponse}->{result}->{success};

Adds one new individual object to your organization's data. This takes as
input an optional extra L<SOAP::Header> object followed by a C<hash> or
C<hashref> representing the object you wish to add to your organization.
The hash must contain the C<type> key in order to identify the type of the
record to add.

Returns a L<SOAP::Lite> object. Success of this operation can be gleaned from
the envelope result as shown in the examples above.

=cut

sub create {
    my $self = shift;
    my $header = ($_[0] && CORE::ref($_[0]) && Scalar::Util::blessed($_[0]) && $_[0]->isa('SOAP::Header')) ? shift : undef;
    $header->uri($SF_URI) if $header;
    my $args;
    if (@_ == 1 && CORE::ref($_[0])) {
        my %copy = eval { %{ $_[0] } }; # try shallow copy
        Carp::croak("Argument to create() could not be dereferenced as a hash") if $@;
        $args = \%copy;
    }
    elsif (@_ % 2 == 0) {
        $args = {@_};
    }
    else {
        Carp::croak("create() got an odd number of elements");
    }
    Carp::croak('Expected a hash object') unless keys(%{$args});

    my $client = $self->_get_client(1);
    my $method = SOAP::Data
        ->name("create")
        ->prefix($SF_PREFIX)
        ->uri($SF_URI)
        ->attr({'xmlns:sfons' => $SF_SOBJECT_URI});

    my $type = $args->{type};
    delete $args->{type};
    Carp::croak('No object type defined to create') unless $type;

    my @elems;
    foreach my $key (keys %{$args}) {
        push @elems, SOAP::Data
            ->prefix('sfons')
            ->name($key => $args->{$key})
            ->type(WWW::Salesforce::Constants->type($type, $key));
    }

    my @headers = ($self->_get_session_header());
    push(@headers, $header) if $header;
    my $r = $client->call(
        $method => SOAP::Data
                    ->name('sObjects' => \SOAP::Data->value(@elems))
                    ->attr({'xsi:type' => 'sfons:' . $type}),
        @headers
    );
    unless ($r) {
        die "could not call method $method";
    }
    if ( $r->fault() ) {
        die( $r->faultstring() );
    }
    return $r;
}

@dclendenan
Copy link
Contributor

coalescing them seems good to me, though in that case I think we'll want a good bit more unit testing of the new potential flows...

@genio
Copy link
Member

genio commented Nov 4, 2019

Unfortunately, it's difficult to mock the SOAP endpoints (man, I hate SOAP). That makes it hard to unit test well. I'll be doing some testing against our SF sandbox area to be sure before anything gets merged/released.

Please do the same on your end and let me know if you run into any pitfalls.

@genio
Copy link
Member

genio commented Sep 10, 2021

Now that we have CI that actually tests against a dev instance I setup, and the version of the API is choosable easily in the constructor in the release I just cut out there, I'll get back to this.
I think I still prefer the way in the comment above if there are no objections

@genio
Copy link
Member

genio commented Sep 10, 2021

Though a "reserved" hash key may make more sense:

{
  -headers => [$some_headers_obj, $some_other_header],
  type => 'Account',
  name => 'foo bar',
}

@genio
Copy link
Member

genio commented Sep 10, 2021

I'm not a huge fan of this, but it seems like the best way to implement allowing headers for the various methods that allow them in Salesforce's API. It's eww, but poor design decisions way back when have us here (blame my younger self).

sub _get_method_headers {
    my $self = shift;
    return () unless @_;
    if (@_ == 1 && CORE::ref($_[0]) eq 'ARRAY') {
        return grep {$_ && CORE::ref($_) eq 'SOAP::Header'} @{$_[0]};
    } elsif (@_ == 1 && $_[0] && CORE::ref($_[0]) eq 'SOAP::Header') {
        return($_[0]);
    }
    return grep {$_ && CORE::ref($_) eq 'SOAP::Header'} @_;
}

sub _get_session_header {
    my ($self) = @_;
    return SOAP::Header->name('SessionHeader' =>
            \SOAP::Header->name('sessionId' => $self->{'sf_sid'}))
        ->uri($SF_URI)->prefix($SF_PREFIX);
}

sub create {
    my $self = shift;
    my $args;
    if (@_ == 1 && CORE::ref($_[0])) {
        my %copy = eval { %{ $_[0] } }; # try shallow copy
        die("Argument to create() could not be dereferenced as a hash") if $@;
        $args = \%copy;
    }
    elsif (@_ % 2 == 0) {
        $args = {@_};
    }
    else {
        die("create() got an odd number of elements");
    }
    die('Expected a hash object') unless keys(%{$args});

    # parse headers
    my @headers = ($self->_get_session_header(), $self->_get_method_headers($args->{-headers}));
    delete($args->{-headers});
    
    ...
    
}

@DonLewisFSI
Copy link
Author

Is this available anywhere for testing?

@genio
Copy link
Member

genio commented May 17, 2022

Nah. That ended up not behaving as hoped. There's also nothing that indicates what the headers should look like from Salesforce's documentation. So, it got left by the wayside for a while. SOAP is evil

  • I probably should have indicated for anyone reading later that we're talking about SOAP envelope headers here, not HTTP headers. There's nothing that explains what these headers should look like as many of them can contain multiple entries or "arrays" of information.

@DonLewisFSI
Copy link
Author

DonLewisFSI commented Jun 1, 2022

The Salesforce way seems to be to make the headers a persistent attribute of the connection. Scroll down for a code example: https://developer.salesforce.com/docs/atlas.en-us.236.0.api.meta/api/sforce_api_objects_lead.htm

There is a setAssignmentRuleHeader() method for assignment rules, and I suppose methods for other header types.

Can't say that I'm a fan ...

Actually, this is only true for the java API. The C# API just wants all the possible headers (or nulls to omit those header types) to be passed as the first args to create().

Neither seems to try to do any smart guessing about what is being passed.

@DonLewisFSI
Copy link
Author

Here is some generic SOAP header documentation: https://www.tutorialspoint.com/soap/soap_header.htm

@DonLewisFSI
Copy link
Author

This PHP API implementation resembles the Salesforce Java API: https://github.com/forceworkbench/forceworkbench

workbench/soapclient/SforceHeaderOptions.php contains the methods that capture the optional header info.

workbench/soapclient/SforceBaseClient.php contains the code that outputs the headers specific to each API call such as create().

@genio
Copy link
Member

genio commented Jun 1, 2022

Unfortunately, Salesforce's documentation doesn't actually show any rendered XML examples. They show me code examples in other languages that don't really help at all if you're not using one of those languages. They don't provide any examples of what the XML-rendered headers look like.

What I can do:

I'd have to use another language. I'd have to pull in the WSDLs in that language. I'd have to setup my own mock Salesforce endpoint that got through the login step. I'd have to setup a mock endpoint for the various calls that accept the various headers. I'd have to have the client in one of the other languages hit that mock service on each of those endpoints in each of the various ways to add content to those headers. I'd have to have the mock service dump out the request to be able to then see what the rendered XML looks like. Then, I'd be able to write accurate tests for the rendering from the Perl app to be able to accurately handle these SOAP headers.

Less effort:

Or, I'd have to re-write this entire service using anything other than SOAP::Lite so I could pull in the WSDLs and have the client from the XML::Compile::* modules build the requests themselves. Rewriting that way, I might as well forego touching SOAP all together and write a REST client. If I do either of those, I'm breaking the way this old module works as the responses from method calls are most times SOAP::Lite response objects.

What have I done?!

I made lots of bad decisions when originally making this module ~20 years ago and now I've got myself in a no-win situation design-wise. Not a whole lot I can do other than the lots-of-effort mocking method since I can't "fix" the module without breaking it for anyone using it.

@DonLewisFSI
Copy link
Author

Hmn, I'd hoped the PHP version would have the code for rendering, but it turns out that is just passes the array of headers to SoapClient::__setSoapHeaders(). The perl version would have to pass the headers to SOAP::Header(), https://metacpan.org/dist/SOAP-Lite/view/lib/SOAP/Header.pod , for each request.

I found some example XML here; https://developer.salesforce.com/blogs/developer-relations/2015/06/salesforce-soap-api-sample-wsdls

The perl code already knows enough about headers to insert the SessionHeader. The perl code also knows how to pass multiple headers, since query() does it to pass both the SessionHeader and the batchSize header for the limit implementation.

@genio
Copy link
Member

genio commented Jun 4, 2022

master...soapHeaders meh

I'm still not thrilled with the implementation, but I think I dislike this attempt less than the others. thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants