Building OData $filter URLs with an Angular provider

10,502

Solution 1

You can use odata-filter-builder to build $filter part for OData URL query options.

Then just use $http with config params.

Short example:

var filter = ODataFilterBuilder()
      .eq('id', 2)
      .eq('city', 'Los Angeles')
      .and('substringof("Lagasse", chef)')
      .toString();

$http
  .get(resourceUrl, {params: {$filter: filter, $top: 20}})
  .then(function(response) {
    // Handle response
  })

Full example:

angular
  .module('OData', [])
  .constant('ODataFilterBuilder', ODataFilterBuilder)
  .factory('ODataService', function($http) {
    return {
      load: function(resourceUrl, queryParams) {
        return $http.get(resourceUrl, {params: queryParams})
          .then(function(response) {
            return response.data.value;
          });
      }
    }
  });

angular
  .module('app', ['OData'])
  .controller('appController', function($http, ODataService, ODataFilterBuilder, $httpParamSerializer) {

    // 1. inject ODataFilterBuilder
    // use short name for filter builder
    var f = ODataFilterBuilder;

    // 2. build filter
    var filter = f()
      .eq('id', 2)
      .eq('city', 'Los Angeles')
      .and('substringof("Lagasse", chef)')

    // 3. creater odata query params
    var queryParams = {
      $filter: filter.toString(),
      $top: 20
      // TODO: add other params
    };

    // 4. prepare odata resourse URL
    var odataServiceUrl = 'http://path/to/odata/service/';
    var odataResourseUrl = odataServiceUrl + 'entity';

    // 5. Do http request with odataResourseUrl and queryParams
    // use ODataService or angular $http service

    // ODataService
    //  .load(odataResourseUrl, queryParams)
    //  .then(function(value) {
    //    // handle value
    //  });

    // OR

    // $http.get(odataResourseUrl, {params: queryParams})
    //  .then(function(respons) {
    //    // handle respons.data.value
    //  });


    // Result examles:

    // NOTE: $httpParamSerializer - default $http params serializer that converts objects to strings
    var queryParamsSerialised = $httpParamSerializer(queryParams);

    // put values to 'this' to use it in html
    this.queryParams = queryParams;
    this.queryParamsSerialised = queryParamsSerialised;
    this.queryUrl = odataResourseUrl + '?' + queryParamsSerialised;
  });
<div ng-app="app" ng-controller="appController as vm">
  <pre>queryParams: {{vm.queryParams|json}}</pre>
  <pre>queryParamsSerialised: {{vm.queryParamsSerialised}}</pre>
  <pre>queryUrl: {{vm.queryUrl}}</pre>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="https://npmcdn.com/odata-filter-builder@^0.1/dist/odata-filter-builder.js"></script>

Solution 2

I know you said you are not using any other library, but in case anyone else stumbles up this...I suggest using joData. It's very expressive and requires no other dependencies: https://github.com/mccow002/joData

Here is your example in joData:

var query = new jo('http://www.example.com/odata/Restaurants');
    query.filter(new jo.FilterClause('id').eq(2))
    .andFilter(new jo.FilterClause('city').eq('Los Angeles'))
    .andFilter(new jo.FilterClause('chef').substringof('Lagasse').eq(true))
    .top(2);

Then,

query.toString()

produces...

"http://www.example.com/odata/Restaurants?$top=2&$filter=id eq 2 and city eq 'Los Angeles' and substringof('Lagasse',chef) eq true"

Anyway, to set this up for Angular here is what I did...

  1. Reference joData js in your html before your angular.js reference.

  2. Wrap it in an Angular service

    angular.module('app.shared.oData', []) .service('oDataBuilderService', function () { this.jo = jo; };

  3. Now other services can use this odata service to construct odata queries:

    angular.module('app.shared.video', []) .service('videoService', function ($http, oDataBuilderService) {

         this.getByVideoCatalogId = function (videoCatalogId) {
            var query = new oDataBuilderService.jo('http://www.example.com/VideoCatalog/OData');
            var videoCatalogIdEquals = new oDataBuilderService.jo.FilterClause("VideoCatalogId").eq(videoCatalogId);
            query.andFilter(videoCatalogIdEquals);
            var queryAsString = query.toString();
            var promise = $http.get(queryAsString);
            return promise;
        }
    

    });

Solution 3

You can add $filter= at the end, but then you'd have the same problem with the "and "

Easier would be to just create a simple function

addFilter(currentFilter, filter) {
    if (currentFilter.length == 0) {
        currentFilter = "$filter=" + filter;
    } else {
        currentFilter += "and " + filter;
    }
    return currentFilter;
}

so then just call currentFilter = addFilter(currentFilter, "what to filter")

var currentFilter = "";
if (city) {
  currentFilter = addFilter(currentFilter, "city eq #{cityName}");
}
if (chef) {
  currentFilter = addFilter(currentFilter, "substringof(#{chefName}, chef)");
}

Solution 4

I've done similar and have what I think is an elegant solution with no dependencies on other libraries. I use Array.join and concatenate the filters with an "and" between them:

var filter = [];
if (city) filter.push("City eq '" + city + "'");
if (chef) filter.push("Chef eq '" + chef + "'");
var filterQuery = filter.join(" and ");

This assumes you always want "and" between them, (not or), but that is the case in my apps and in your example. I also do this to build up the url:

var parts = [baseurl];
parts.push("$top=10");
parts.push("$skip=" + skip);
parts.push(filterQuery);
var url = parts.join("&");
Share:
10,502
twinb
Author by

twinb

Updated on June 26, 2022

Comments

  • twinb
    twinb almost 2 years

    I have an angular provider for querying an OData service.

    Now I'm trying to build a $filter function onto that provider, so I can use it throughout my app.

    The problem I'm running into, and so far haven't been able to solve, is that the query part of the URL needs to start with '$filter=' which I can handle fine when there is one filter, but multiple filters are added as ' and {query goes here}'.

    A sample query would be:

    http://www.example.com/odata/Restaurants/?$filter=id eq 2 and city eq 'Los Angeles' and substringof("Lagasse", chef)&$top=20
    

    I'm sending all the filters in an array to the provider. Ideally, I would use "$filter=#{firstFilter}" for the first filter in the array and for the remaining filters use " and #{remainingFilter}" but I'm unsure how to structure this code.

    My current code uses multiple if statements to check if a filter is there, but with the nature of building the url, it makes one of the filters mandatory at all times. I'd like to avoid this.

    For example:

    var filter = "$filter=id eq 2";
    if (city) {
      filter += " and city eq #{cityName}";
    }
    if (chef) {
      filter += " and substringof(#{chefName}, chef)";
    }
    

    Now everytime a user inputs a query, they have to specify an id.

    We are NOT using BreezeJS, JayData, or any other library. Strictly AngularJS and specifically $http, NOT $resource.

  • twinb
    twinb over 9 years
    Trying to implement this now. Confused about the second block of code. In the controller I'm sending an array of strings to the provider that it can iterate through to build the URL. I want to abstract as much of the code away to the controller as possible.I'll keep trying things with this answer and hopefully get it working.
  • John
    John over 9 years
    @twinb you should have a service that does this stuff, not the controller. That way its more reusable. Depending on what your array of filters looks like you should just be able to call something like angular.forEach(filters, function(filter, index) { currentFilter = addFilter(currentFilter, filter); })
  • John
    John over 9 years
    @twinb also I just noticed I had a typo and wasn't sending in currentFilter into the function, that should help!
  • twinb
    twinb over 9 years
    I also had a typo, meant that I wanted to move everything to the service(technically right now it's a provider), not the controller, so that all the controller did was call the service and send an array for it to handle.
  • John
    John over 9 years
    @twinb is it working for you or do you still have questions on how to use it?
  • twinb
    twinb over 9 years
    Can't quite get it working. Still sends the base url as a query. One thing I should have noted in my original question is that it already has a base url, which in this case would be equivalent to currentFilter(I assume). essentially: currentFilter = "http.example.com/odata/Restaurants/". I don't think this would make a difference though. I should've made a plunker.
  • John
    John over 9 years
    Ok in that case right before you make the call append currentFilter to your baseUrl. If you send it through (or set currentFilter to it) it will assume that $filter= has been set. So baseUrl=http://url/for/api/ ... $http.get(baseUrl + currentFilter) or similar. Let me know if that helps
  • John
    John over 9 years
    @twinb forgot to tag you in my last message