Skip to content

Latest commit

 

History

History
298 lines (228 loc) · 10.7 KB

API-mapping.md

File metadata and controls

298 lines (228 loc) · 10.7 KB

Customizing the API Mapping

All HTTP requests made by ng-admin to your REST API are carried out by Restangular, which is like $resource on steroids.

The REST specification doesn't provide enough detail to cover all requirements of an administration GUI. ng-admin makes some assumptions about how your API is designed. All of these assumptions can be overridden by way of Restangular's request and response interceptors.

That means you don't need to adapt your API to ng-admin; ng-admin can adapt to any REST API, thanks to the flexibility of Restangular.

Entry Format

Ng-admin expects that requests for a single entity return a JSON object with all the properties defined as fields. For instance, for the following definition:

var bookEntity = nga.entity('books');
bookEntity.editionView()
    .fields([
        nga.field('name'), 
        nga.field('author_id', 'reference')
            .label('Author')
            .targetEntity(author)
            .targetField(nga.field('name')),
        nga.field('publication_date', 'date')
    ]);

ng-admin expects the GET http://your.api.domain/books/12 route to return a JSON response with a single object, containing at least the following properties:

{
    "id": 12,
    "name": "War and Peace",
    "author_id": 345,
    "publication_date": "2014-01-01T00:00:00Z",
}

Additional properties not defined in the view are ignored.

Now if your API returns results in another format, for instance with all the values under a values key:

{
    "values": {
        "id": 12,
        "name": "War and Peace",
        "author_id": 345,
        "publication_date": "2014-01-01T00:00:00Z"
    }
}

You can use Restangular element transformers to map that to the expected format:

myApp.config(['RestangularProvider', function(RestangularProvider) {
    RestangularProvider.addElementTransformer('books', function(element) {
        for (var key in element.values) {
            element[key] = element.values[key];
        }

        return element;
    });
}]);

Symetrically, if your API requires that you post and put data inside of a values field, use a Restangular request interceptor:

myApp.config(['RestangularProvider', function(RestangularProvider) {
    RestangularProvider.addFullRequestInterceptor(function(element, operation, what, url, headers, params, httpConfig) {
        if(operation == 'post' || operation == 'put') {
            element = { values: element };
        }
        return { element: element };
    });
}]);

Tip: If you want to define a field mapped to a deeply nested property, you don't need an interceptor. Just define the field with a name made by the path to the property using dots as separators:

{
    "id": 12,
    "name": "War and Peace",
    "author": {
        "id": 345,
        "name": "Leo Tolstoi"
    },
    "publication_date": "2014-01-01T00:00:00Z"
}
listView.fields([
    nga.field('name'),
    nga.field('author.name')
])

Custom Headers, Authentication

Does your API require an authentication header? Restangular has got you covered with setDefaultHeaders:

myApp.config(['RestangularProvider', function(RestangularProvider) {
    var login = 'admin',
        password = '53cr3t',
        token = window.btoa(login + ':' + password);
    RestangularProvider.setDefaultHeaders({'Authorization': 'Basic ' + token});
}]);

HTTP Method

The REST standard suggests using the POST method to create a new resource, and PUT to update it. If your API uses a different verb for a given action (e.g. PATCH), then you can force the method to be used for a given entity with createMethod() and updateMethod(). The method name should be in lowercase.

bookEntity.createMethod('put');   // default is 'post'
bookEntity.updateMethod('patch'); // default is 'put'

Pagination

ng-admin assumes that your API accepts _page and _perPage query parameters to paginate lists:

http://your.api.domain/entityName?_page=2&_perPage=20

For instance, to use offset and limit instead of _page and _perPage across the entire application, use the following code:

myApp.config(['RestangularProvider', function(RestangularProvider) {
    RestangularProvider.addFullRequestInterceptor(function(element, operation, what, url, headers, params, httpConfig) {
        if (operation == 'getList' && what == 'entityName') {
            params.offset = (params._page - 1) * params._perPage;
            params.limit = params._perPage;
            delete params._page;
            delete params._perPage;
        }
        return { params: params };
    });
}]);

The default number of items per page is 30. You can customize it with the perPage() method of the listView.

For each list view, the API should should return all items as an array at the root of the response.

GET /entityName?_page=2&_perPage=20 HTTP1.1

HTTP/1.1 200 OK
X-Total-Count: 66
Content-Type: application/json
[
  { date: "2014-01-01T00:00:00Z", "name": "foo", "age": 8 },
  { date: "2014-02-01T00:00:00Z", "name": "bar", "age": 12 },
  { date: "2014-03-01T00:00:00Z", "name": "baz", "age": 4 },
  ...
]

Total Number of Results

To know how many items to paginate through, ng-admin retrieves the total count from the X-Total-Count header (see example above). That's what's used to build up the pagination controls. For instance, based on the request and response from the example above (page 2, 20 items per page, 66 items in total), the pagination controls will look like:

    <previous  1  [2]  3  4  next>

If your API doesn't return a X-Total-Count header, you can add a totalCount property to the response object using a Restangular response interceptor. For instance, for a rfc7233-compliant API returning pagination data using a Content-Range header:

Content-range: entities 21-40/66  

Add the following response interceptor:

myApp.config(['RestangularProvider', function(RestangularProvider) {
    RestangularProvider.addResponseInterceptor(function(data, operation, what, url, response) {
        if (operation == "getList") {
            var contentRange = response.headers('Content-Range');
            response.totalCount = contentRange.split('/')[1];
        }
        return data;
    });
}]);

Tip: If you added the X-Total-Count header to your API but the pagination controls don't appear, maybe it's a cross-origin resource sharing (CORS) problem. The admin application is probably hosted on another domain than the API, so you must explicitly allow the access to the header from the admin app domain. To do so, add a Access-Control-Expose-Headers: x-total-count header to the API response.

Sort Columns and Sort Order

To sort each list view, ng-admin uses _sortField & _sortDir query parameters.

http://your.api.domain/entityName?_sortField=name&_sortDir=ASC

Once again, you can change it with a response interceptor. For instance, to sort by id desc by default, and without changing the name of the sort query parameters, use:

myApp.config(['RestangularProvider', function(RestangularProvider) {
    RestangularProvider.addFullRequestInterceptor(function(element, operation, what, url, headers, params, httpConfig) {
        if (operation == 'getList' && what == 'entityName') {
            params._sortField = params._sortField || 'id';
            params._sortDir = params._sortDir || 'DESC';
        }
        return { params: params };
    });
}]);

(If a column is not sortable by your backend, e.g. a computed column, you can disable sorting per-column using Field.sortable(false).)

Filtering

All filter fields are added as a serialized object passed as the value of the _filters query parameter. For instance, the following filterView() configuration:

myEntity.filterView()
    .fields([
        nga.field('q').label('').attributes({ placeholder: 'Full text' }),
        nga.field('tag')
    ]);

...will lead to API calls formatted like the following:

http://your.api.domain/entityName?_filters=%7b%22q%22%3a%22foo%22%2c%22tag%22%3a%22bar%22%7d

Where the _filters value is the url encoded version of {"q":"foo","tag":"bar"}.

Just like other query params, you can transform it using a Restangular request interceptor. For instance, to pass all filters directly as query parameters:

myApp.config(['RestangularProvider', function(RestangularProvider) {
    RestangularProvider.addFullRequestInterceptor(function(element, operation, what, url, headers, params, httpConfig) {
        if (operation == 'getList' && what == 'entityName') {
            if (params._filters) {
                for (var filter in params._filters) {
                    params[filter] = params._filters[filter];
                }
                delete params._filters;
            }
        }
        return { params: params };
    });
}]);

Now, the API call URLs will look like:

http://your.api.domain/entityName?q=foo&tag=bar

Nested Relationships Urls

By default, ng-admin uses filters to fetch entities related to another one. For instance, to fetch all the comments related to the post entity #123, ng-admin calls the following url:

http://[baseApiUrl]/comments?_filters={"post_id":123}

Some API servers only support a special type of URL for that case:

http://[baseApiUrl]/posts/123/comments

Restangular doesn't allow to modify the URL of an outgoing request (see Restangular issue #603), so in order to achieve that you must use an interceptor on the $http Angular service.

myApp.config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.push(function() {
        return {
            request: function(config) {
                // test for /comments?_filters={post_id:XXX}
                if (/\/comments$/.test(config.url) && config.params._filters && config.params._filters.post_id) {
                    config.url = config.url.replace('comments', 'posts/' + config.params._filters.post_id + '/comments');
                    delete config.params._filters.post_id;
                }
                return config;
            },
        };
    });
}]);

Identifier

ng-admin assumes that the identifier name of your entities is id. You can change it with the identifier() method of the entity.

var post = nga.entity('posts')
    .identifier(nga.field('_id'));

Date

The default date field format is yyyy-MM-dd. You can change it with the format() method in fields of type date.

nga.field('publication_date', 'date').format('dd MM yyyy')