Why is data variable undefined in resource query transform
Asked Answered
C

2

6

I am new to angular...so I am sure I am doing something wrong. Spent hours trying to search for a solution but the angularjs documentation is next to useless...and pretty much every example out there tries to set this at a global level.

I am trying to pass a complex javascript object as a query prameter. The object has a property that is an array and so I need to flatten (proper term ??) this object so MVC binding can correctly instantiate the model.

The object I am trying to pass is something along the lines of

newRequest = {
   SearchTerms: 'error',
   PageSize: 25,
   ...
   Facets: [
       { Field: 'createdBy', Value: 'Joe' },
       { Field: 'createdBy', Value: 'Mary' }
   ]
}

I declared my resource as follows

(function () {
    "use strict";

    angular
        .module('common.services')
        .factory('searchResource', ['$resource', 'appSettings', searchResource]);

    function searchResource($resource, appSettings) {
        return $resource(appSettings.searchPath, null, {
            query: {
                method: 'GET',
                transformRequest: function (data, headersGetter) {
                    if (data === undefined) {   // this is always true
                        return data;
                    }

                    return $.param(data);
                }
            }
        });
    }
}());

And I am using it with

vm.executeSearch = function () {
    searchResource.query(
        newRequest,
        function (data) {
            vm.response = data;
            vm.request = data.Request;
        }
    );
}

The transformRequest function is being called...and headersGetter has a value.

Additional Info

As suggested, I changed direction and instead of using a resource I went with a service via factory. Same result...the data parameter is undefined. Here is that new code.

(function () {
    "use strict";

    angular
        .module('common.services')
        .factory('searchProvider', ['$http', 'appSettings', searchProvider]);

    function searchProvider($http, appSettings) {
        return {
            query: function (request, callback) {
                $http({
                    url: appSettings.searchPath,
                    method: 'GET',
                    params: request,
                    transformRequest: function (data, headersGetter) {
                        if (data == undefined) {
                            return data;
                        }

                        return $.param(data);
                    }
                })
                .success(function (data) {
                    callback(data);
                });
            }
        }
    }


}());

searchProvider.query(
    newRequest,
    function (data) {
        console.log(data);

        vm.response = data;
        vm.request = data.Request;
    }
);

But the problem is that data is undefined! I know the newRequest object is valid because the call goes out. Is just has an improperly formatted url. Where did I go wrong?

Chintz answered 3/10, 2015 at 2:46 Comment(1)
To learn more about $resource, this is a good read: blog.mgechev.com/2014/02/05/…Kairouan
L
3

The problem is that it's a GET request and ngResource won't accept data in a GET request:

See https://github.com/angular/angular.js/blob/master/src/ngResource/resource.js#L526

var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method);

It uses hasBody to determine if to set the data parameter or not.

So at this point your best bet is to move to using $http since ngResource is for a more RESTful api or switch your API to POST instead of GET.

Edit

And another thing: you don't need to use if (data === undefined) when you can do : if(!data) which means the same thing.

Javascript thruthiness : https://developer.mozilla.org/en-US/docs/Glossary/Truthy

Reply to the additional info

You're still getting undefined in data because you're not setting data to anything, you're setting params. But once you're here you don't need to transform the request.

You can just do

$http({
  ...
  params: $.param(request),
  ...
})
Lanti answered 3/10, 2015 at 3:23 Comment(3)
Thank you @sirrocco. I will test it out over the weekend and respond back/mark answer on Monday.Chintz
Yeah. I was aware of that...that particular piece of code was copy/pasted from another some marked as correct answer somewhere. Did not want to make changes until I confirm that my original goal was working. That being said...I converted my resource to a service (provider??) via factory and I get the same thing -- data is undefined. Will update my original question with modified code for review.Chintz
Yes, still null because you're sending params not data. But you don't need the transform at this point, just transform the params before you make the request.Lanti
P
1

There are a couple bits of extra info that would help us get right at the source of the issue. I think we can get by without them, but in case we get stuck, I would like to know:

  • Are there any errors in the browser console?
  • What does the search endpoint URL appSettings.searchPath look like?
    • Does it contain colon-prefixed parameters?
  • What kind of request is your endpoint expecting?
  • Are you including jQuery or jQuery Lite, in addition to Angular?

Just to get things going, I'm going to assume you are not actually getting any console errors, and you've included the necessary libraries for both Angular and jQuery.

If that's the case, here are some tidbits that may help.


When you use the $request factory, you produce a sort of interface that abstracts making HTTP requests. Since you're using this for "search", let's call this your search interface.

If you perform a GET request via your search interface's query method, you can pass in any Object or Array and it will build a URL with a query based on that argument.

For example, let's say you passed in your newRequest variable as-is, without any processing. That would end up producing a URL query like the following (decoded for readability):

?Facets={"Field":"createdBy","Value":"Joe"}&Facets={"Field":"createdBy","Value":"Mary"}&PageSize=25&SearchTerms=error

Notice how a Facets parameter is specified twice? Once for each element in your Facets array. This might actually work, if your end-point supports requests in that format.

On the other hand, using jQuery's $.param utility, as you've attempted to do in this snippet from your first example...

{
    query: {
        method: 'GET',
        transformRequest: function (data, headersGetter) {
            if (data === undefined) {
                return data;
            }

            return $.param(data);
        }
}

... you would in theory end up with a URL query string like this (decoded for readability):

SearchTerms=error&PageSize=25&Facets[0][Field]=createdBy&Facets[0][Value]=Joe&Facets[1][Field]=createdBy&Facets[1][Value]=Mary

Why "in theory?" Well, as it turns out, you can't use $request this way, for a few reasons.

'query' uses HTTP GET

Firstly, the HTTP method you specify for query, will dictate what happens to the parameter object you pass in to that query method.

Assuming query uses HTTP GET (as in your examples), the parameter object will be serialized and appended to the end-point URL. You will end up with a URL that has something like this tacked onto it:

?Facets={"Field":"createdBy","Value":"Joe"}&Facets={"Field":"createdBy","Value":"Mary"}&PageSize=25&SearchTerms=error

By using HTTP GET, you are basically saying that the data you want to send with the request is embedded in the URL.

This happens regardless of any processing you specify in transformRequest.

In fact, since this query action is not an HTTP POST (it's a GET), there is no data to "post," because that's how GET works differently than POST (read up on HTTP methods, if this is new territory for you). That's why your transformRequest function gets a data argument that is undefined.

What if we used HTTP POST?

Let's assume you set up query to use HTTP POST. In that case, the parameter object you pass in when calling query will be put in the post data (aka. request payload) of your HTTP request. It will not be appended to the URL, as had been done with GET.

By using HTTP POST, you are now declaring that the data you want to send can be found in the request body (not the URL).

Since there is "data" to post, the parameters object you pass into query will be available as the data argument in your transformRequest function. This is good, because you can't just send in a raw object like newRequest. You will need to serialize it into a string. Otherwise you'll end up with post data like:

[object Object]

But since this is now a POST, you don't have to format it like a URL query anymore (since it is not going onto the URL). You can use whatever data format your end-point wants to receive. If you're end-point accepts JSON, you could send a JSON encoded version of your newRequest object, by running something like this in your transformRequest function:

return JSON.stringify(data)

But what if we must use GET?

Sometimes you have no control over the end-point. What if it only takes GET requests for that search API you want to use? And what if it wants a very specific URL query format?

Not a problem... up to a point.

When you create your search interface via the $request factory, you can specify a resource URL that is paramterized.

Let's say your search end-point URL was:

/search.do

And it want's to see GET requests with URLs like:

/search.do?PageSize=25&SearchTerms=error

You would then create your search interface with a paramaterized URL, something like this:

$resource("/search.do?PageSize=:PageSize&SearchTerms=:SearchTerms")

Such that you could pass in a request like:

searchResource.query({PageSize: 25, SearchTerms: "error"}, successCallback);

Which would make a GET request with a URL:

/search.do?PageSize=25&SearchTerms=error

Unfortunately this starts to break down with unbounded parameter lists like your Facets array. The preceding pattern requires that you have a fixed list of arguments.

Let's not forget that if your end-point is not too picky, then you could always just send a GET request with a URL serialized like this:

?Facets={"Field":"createdBy","Value":"Joe"}&Facets={"Field":"createdBy","Value":"Mary"}&PageSize=25&SearchTerms=error

which is what $resource does to your newRequest object for free.

Let's get complicated

What if your really, REALLY want to use $request, and you have to send GET requests that look like the output of $.param?

Okay. Let's get fancy. JS fancy.

If you are only ever going to use the query method on your search interface, you can just wrap the object produced by $resource.

.factory("searchResource", ["$resource", searchResource]);

function searchResource($resource) {
    var searchDelegate = $resource("/search.do?:queryString");
    var searchInterface = {
        query: function (params) {
            var encodedQuery = $.param(params);
            return searchDelegate.query({queryString: encodedQuery});
        }
    };
    return searchInterface;
}

Now you can pass in your newRequest object as-is, and your wrapped search resource will make a GET request to this URL (decoded for readability):

search.do?SearchTerms=error&PageSize=25&Facets[0][Field]=createdBy&Facets[0][Value]=Joe&Facets[1][Field]=createdBy&Facets[1][Value]=Mary

Pants answered 3/10, 2015 at 23:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.