Accessing complex REST resources with Ext JS
Asked Answered
L

2

14

I am accessing a REST service which exposes these two resources, a parent resource and a child resource:

/users
/users/{userId}/account

So the resource "account" is not nested within the resource "user", it has to be accessed by a second request. There are examples for such REST APIs, e.g. here

I use these models to map users and their account to the Ext Js 4 data model:

User

Ext.define("MyApp.model.User", {
    extend: "Ext.data.Model",
    fields: [ { name: "id", type: "string" }],
    associations: [{
            model: "MyApp.model.Account",
            name: "account",
            type: "hasOne",
            reader: "json",
            getterName: "getAccount",
            setterName: "setAccount", 
            foreignKey: "accountId"
        }
    ],
    proxy: {
        type: "rest",
        url: "/rest/users",
        reader: {
            type: "json",
            totalProperty: "total",
            root: "users"
        }
    }
});

Account

Ext.define("MyApp.model.Account", {
    extend: "Ext.data.Model",
    fields: [ { name: "id", type: "string" }],
    belongsTo: "MyApp.model.User",
    proxy: {
        type: "rest",
        reader: { type: "json"}
    }
});

The account proxy does not have a url (I hoped this would be created based on the parent user model). When I call user.getAccount() I get an exception because the proxy is missing the url.

Question: Is there some way to setup the models such that Ext Js will access /users/{userId}/account WITHOUT updating the account proxy url manually with each parent userId?

Lungan answered 25/7, 2013 at 9:43 Comment(2)
Does the Account model really have its own id? That doesn't seem necessary if there is a one to one relation between account and user.Barela
that's right, the account id is not required.Lungan
B
26

You won't get what you want from stock Ext classes, you'll have to get a little bit dirty...

From what I understand you need the id of the user to load its account, not the id of the account record itself. So, I would configure the association to reflect that:

associations: [{
    model: "MyApp.model.Account",
    name: "account",
    type: "hasOne",
    reader: "json",
    getterName: "getAccount",
    setterName: "setAccount",
    // foreignKey: "accountId"
    foreignKey: 'id'
}],

The big advantage here being that the user id will be available to the proxy when it will be asked to build the url for the request.

Now, in order to build the url with the format you need, we must replace the proxy's buildUrl method. And, as you've already discovered, you do need an url to get to this method in the first place.

So, here's how I would configure the Account proxy:

proxy: {
    type: "rest",
    reader: {type: "json"},

    // Give it an URL to avoid the error
    url: '/rest/users/{}/account',

    // Replace the buildUrl method
    buildUrl: function(request) {
        var me        = this,
            operation = request.operation,
            records   = operation.records || [],
            record    = records[0],
            url       = me.getUrl(request),
            id        = record ? record.getId() : operation.id;

        // Here's the part honoring your URL format
        if (me.isValidId(id)) {
            url = url.replace('{}', id);
        } else {
            throw new Error('A valid id is required');
        }

        // That's enough, but we lose the cache buster param (see bellow)
        return url;

        // If we want the cache buster param (_dc=...) to be added,
        // we must call the superclass, which will read the url from
        // the request.
        request.url = url;
        return Ext.data.proxy.Rest.superclass.buildUrl.apply(this, arguments);
    }
}

At this point, we end up with the proxy firing request on urls of the form:

rest/users/45/account?id=45

That's only cosmetic but that id query parameter annoys me, so I would also replace the buildRequest method of the proxy with the following one:

buildRequest: function(operation, callback, scope) {
    var me = this,
        params = operation.params = Ext.apply({}, operation.params, me.extraParams),
        request;

    Ext.applyIf(params, me.getParams(operation));

    // if (operation.id !== undefined && params[me.idParam] === undefined) {
    //     params[me.idParam] = operation.id;
    // }

    request = new Ext.data.Request({
        params   : params,
        action   : operation.action,
        records  : operation.records,
        operation: operation,
        url      : operation.url,
        proxy: me
    });

    request.url = me.buildUrl(request);

    operation.request = request;

    return request;
}

And, here you are... While it would work, I don't really recommend overriding methods this way, in the proxy's config. In real life, you should extend your own proxy class from the Rest one, especially if you need to configure many such proxies... But I hope I gave you all the ingredients you need to get started!

Barela answered 25/7, 2013 at 12:17 Comment(2)
I am sorry that I can only accept this answer once :-) Your first sentence is most important to me, maybe I should think about changing the rest api. I tested the workaround you posted, it works for me, and I really do not need to "updating the account proxy url manually". Great work!Lungan
I had to use operation.filters[0].value to get the foreign key.Selfpreservation
G
5

I had the same problem and I found rixo's answer definitely amazing. Therefore I adopted it for myself, but then I made some modifications, so this is the code I'm using at the moment. The advantage is that it allows you to format the service URL absolutely as you prefer, concatenating even more that one parameter.

// Replace the buildUrl method
                buildUrl: function (request) {
                    var me = this,
                        url = me.getUrl(request);
                    var added = [];
                    for(var p in request.params)
                    {
                        if (url.indexOf('{' + p + '}') >= 0) {
                            url = url.replace('{' + p + '}', request.params[p]);
                            added.push(p);
                        }
                    }
                    for(var a in added)
                    {
                        delete request.params[added[a]];
                    }

                    // That's enough, but we lose the cache buster param (see bellow)
                    return url;

                    // If we want the cache buster param (_dc=...) to be added,
                    // we must call the superclass, which will read the url from
                    // the request.
                    request.url = url;
                    return Ext.data.proxy.Rest.superclass.buildUrl.apply(this, arguments);
                }

This way you can use an url like "/service/{param1}/{param2}/?abc={param3}" given a "request.params" object like

{ "param1": "value1", "param2": "value2", "param3": "value3" }

and also there's no need to override the "buildRequest" method since any parameter used here is removed from the "params" object and is not concatenated to the query string later.

I hope this helps, any comment welcome!

Giaimo answered 28/8, 2015 at 12:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.