I have some trouble appending a token to the backbone url query string and hope you guys could help me out here. Three things to know,
There is a rest api that expects a token with each request
An nginx backend that does auth, serves the backbone app + proxy req to the api under /api
i'm a new to javascript + backbone :/
The backbone app actually reads the token from a cookie and I need to append this to the request url everytime backbone makes a call. I see this can be done by overriding backbone sync. but it troubles me in a few different things. like, this is what I do
console.log('overriding backbone sync');
var key ="token";
Backbone.old_sync = Backbone.sync
Backbone.sync = function(method, model, options) {
if (method === 'read') {
if (!(model.url.indexOf('?key=') != -1)) {
model.url = model.url + '?key=' + key;
}
} else {
old_url = model.url();
if (!(old_url.indexOf('?key=') != -1)) {
model.url = function() {
return old_url + '?key=' + key;
}
}
}
Backbone.old_sync(method, model, options);
};
model.url was returning a function when its not a "read" method and didn't know how to handle it well and the other trouble is when a consecutive request is made, the token is added twice. I tried to remove it with that indexOf stuff with no luck.
Is there a better way to do this ?
I don't think you need to override sync at all:
var globalKey = 'key123';
var urlWithKey = function(url, key) {
return function() {
return url + "?key=" + key;
};
};
var MyModel = Backbone.Model.extend({
url: urlWithKey('/my/url/', globalKey)
});
If you now create an object and save it, a POST request to my/url/?key=key123 is sent.
I guess you could also override the url method if this is the behavior you need for all of your Backbone models.
A general note: in Backbone most parameters, such as url can be a function or a value. I don't know why in your example it was a function once and a value in another case, but you always must be able to handle both ways if you override some of the internal functions. If you look at Backbone's sourcecode you will see that they use getValue to access these parameters:
var getValue = function(object, prop) {
if (!(object && object[prop])) return null;
return _.isFunction(object[prop]) ? object[prop]() : object[prop];
};
Update: Overloading the url method for all models could work like this:
var globalKey = 'key123';
(function() {
var baseUrl = Backbone.Model.prototype.url;
Backbone.Model.prototype.url = function() {
return this.baseUrl + "?key=" + globalKey;
};
})()
var MyModel = Backbone.Model.extend({
baseUrl: '/my/url/'
});
You could also leave the regular Backbone.Model as it is, and create your own base class. See http://documentcloud.github.com/backbone/#Model-extend for details.
Just set your URL like so:
url : function() {
return "/my/url" + this.key;
}
In your overridden .sync, you only need to set the key property.
Related
This is my C# WebAPI2 controller, which gets hit:
[HttpGet, Route("bycaseidlist/{idArray}")]
public async Task<IHttpActionResult> GetByCaseIdList([FromUri] List<int> idArray)
This is the call:
var idArray = [4,4,2,4];
var url = baseUrl + 'api/cases/bycaseidlist/' + idArray ;
$http.get(url)
The problem is that the API doesn't get the array, it gets ...this:
In other words an array with one value: 0. Why is this happening? How do I fix it? It seems to be in-line with this answer, but it doesn't work. Should I pass it in the body? I feel like I am missing something obvious.
Get ActionMethods can take objects as arguments. However, the default behavior is to look at the body when the parameter is not a .net primitive. In order to force the action method to use a model binder to read the object data from the request, the parameter can be decorated with the [FromUri] or [ModelBinder] attributes. (Note there are other ways to do this that include doing parameter binding rules but that is probably overkill for what you are trying to accomplish here). Here is an implementation that solves the original problem that you were posing.
<script type="text/javascript">
var ajaxCall = function (myArry) {
var ajaxProperties = {};
ajaxProperties.url = "/api/Mul/Mutiply";
ajaxProperties.type = "Get";
ajaxProperties.data = {};
ajaxProperties.data.numbers = myArry;
ajaxProperties.contentType = "application/json";
console.log(ajaxProperties);
ajaxProperties.success = function (data) {
console.log(data);
}
ajaxProperties.error = function (jqXHR) {
console.log(jqXHR);
};
$.ajax(ajaxProperties);
};
var getData = function (e) {
var myArry = new Array();
myArry.push($('input[name=num1').val());
myArry.push($('input[name=num2').val());
ajaxCall(myArry);
return false;
};
</script>
Controller
[HttpGet]
public IHttpActionResult Multiply([FromUri] int[] numbers)
{
int result = 0;
if(numbers.Length > 0)
{
result = 1;
foreach (int i in numbers)
{
result = result * i;
}
}
return Ok(result);
}
}
I think my mistake was using Get. I might be remembering incorrectly (someone confirm if you know offhand), but Get might not be able to take objects as arguments. Anyway, I changed the method to POST and then changed the param to be sent in the request body, rather than the url. It now works. Here is the working code:
[HttpPost, Route("bycaseidlist")]
public async Task<IHttpActionResult> PostByCaseIdList([FromBody] int[] sqlCaseIdArray)
and the call itself:
function runDbCall(url, sqlCaseIdArray){
return $http({
method: 'POST',
url: url,
data: sqlCaseIdArray
});
}
runDbCall(url, sqlCaseIdArray)
I will come back to this when I figure out if the problem was Get not being able to take objects, but I thought it could in url, just not in body...need to clarify. If someone posts an answer just on that part, I will accept, since that's probably the root of the prob.
The function below is being called with no problem. the $save method is being called and my object is being added to mongodb. However, the problem lies in the $location.path() method. For some reason, the $routeParams.quizId is not being taken in account in the URL.
I can see in Chrome that the url it tries to reach is:
GET http://localhost:3000/api/quizes/questions/54a09a248ff7bf9816f272b9 404 (Not Found)
The quizId is missing, the URL should look like this:
http://localhost:3000/api/quizes/quizIdRightHere1234/questions/54a09a248ff7bf9816f272b9
$scope.create = function() {
var question = new Questions({
value: this.value,
type: this.type,
answer: this.answer
});
question.$save({quizId: $routeParams.quizId}, function(response) {
var quizId = $routeParams.quizId;
var url = '/quizes/' + quizId + '/questions/' + response._id;
$location.path(url);
}, function(errorResponse) {
$scope.error = errorResponse.data.message;
});
};
RouteParams should happen after the hash sign... otherwise it's just different folders.
I tried to use withParameters method on query like that:
query.withParameters({ includeLocation: true })
Unfortunately my parameter was not added to url. I use breeze.debug.js and I've found this line in it
//queryOptions = __extend(queryOptions, this.parameters);
Is that a bug ? Is withParameters support taken out ? Or do I do something wrong ?
I use oData
When .withParameters is used, the parameters are added to the URL by the data service adapter, not by the Breeze core. That's why that line is commented out. This allows the parameters to be encoded differently, depending upon the backend that is used.
That's great, but the data service adapter for OData that ships with Breeze 1.4.8 does not handle .withParameters. The WebApi adapter does, but not the OData adapter. We'll make sure it's added in a future release. In the meantime, you can continue to use your workaround.
This oversight/omission is partly because we don't know any OData services that handle custom parameters. If I may ask, what OData service are you using?
It looks like this will hopefully be fixed soon: https://github.com/Breeze/breeze.js/issues/19.
In the meantime, you can use this code as a workaround (kudos to the author of this pull request):
var odataAdapter = breeze.config.getAdapterInstance('uriBuilder', 'OData');
var origBuildUri = odataAdapter.buildUri;
odataAdapter.buildUri = function (entityQuery, metadataStore) {
var uri = origBuildUri(entityQuery, metadataStore);
//Add custom query option support to webapi odata.
//See https://github.com/Breeze/breeze.js/issues/19
if (entityQuery.parameters !== null && typeof entityQuery.parameters === 'object'
&& Object.keys(entityQuery.parameters).length)
{
var queryParams = {};
for (var param in entityQuery.parameters) {
if (/^([^\$].*)$/.test(param)) {
var val = entityQuery.parameters[param];
if (typeof val == 'string') val = "'" + val + "'";
queryParams[param] = val;
}
}
//get the uri without the resourceName
var resourceName = entityQuery.resourceName;
uri = uri.substr(resourceName.length + 1);
//build the new uri
uri = resourceName + toQueryOptionsString(queryParams) + '&' + uri;
}
//Copied from breeze.js OData adapter
function toQueryOptionsString(queryOptions) {
var qoStrings = [];
for (var qoName in queryOptions) {
var qoValue = queryOptions[qoName];
if (qoValue !== undefined) {
if (qoValue instanceof Array) {
qoValue.forEach(function (qov) {
qoStrings.push(qoName + "=" + encodeURIComponent(qov));
});
} else {
qoStrings.push(qoName + "=" + encodeURIComponent(qoValue));
}
}
}
if (qoStrings.length > 0) {
return "?" + qoStrings.join("&");
} else {
return "";
}
}
return uri;
};
I am trying to set up a variant fetch method on my backbone model that will fetch the current model for a given user. This is available from the API on /api/mealplans/owner/{username}/current.
I have written the following model. I commented out the URL Root, as the prototype fetch call was simply using the urlRoot and I wanted to see if that was overriding the url parameter I passed in portions somehow.
var mealPlan = Backbone.Model.extend({
name: 'Meal Plan',
//urlRoot: '/api/mealplans',
defaults: {},
fetchCurrent: function (username, attributes, options) {
attributes = attributes || {};
options = options || {};
if (options.url === undefined) {
options.url = "/api/mealplans/owner/" + username + "/current";
}
return Backbone.Model.prototype.fetch.call(this, attributes, options);
},
validate: function (attributes) {
// To be done
return null;
}
});
I've seen this done, in some variations in other places, such as at backbone.js use different urls for model save and fetch - In that case the code is slightly different (I started with that and broke it down to make it easier for me to read.)
The options object has the url parameter in it fine when I pass it to fetch, but then it seems to ignore it!
I was assuming the same parameters to fetch as to save - This is not the case.
The method signature for fetch ONLY takes 'options' and not 'attributes', hence the url parameter wasn't found.
The model code should look a bit more like this..
var mealPlan = Ministry.Model.extend({
name: 'Meal Plan',
urlRoot: '/api/mealplans',
defaults: {
},
fetchCurrent: function (username, options) {
options = options || {};
if (options.url === undefined) {
options.url = this.urlRoot + "/owner/" + username + "/current";
}
return Backbone.Model.prototype.fetch.call(this, options);
},
validate: function (attributes) {
// To be done
return null;
}
});
I think it is better to override url() method, like below:
var mealPlan = Ministry.Model.extend({
name: 'Meal Plan',
urlRoot: '/api/mealplans',
//--> this gets called each time fetch() builds its url
url: function () {
//call the parent url()
var url=Backbone.Model.prototype.url.call(this);
//here you can transform the url the way you need
url += "?code=xxxx";
return url;
}
...
besides, in your example above I think there is a mistake and you should replace fetchCurrent by fetch
I'm trying to figure out the "correct" way of accomplishing custom update
functions in Backbone.js Models. An example of what I'm trying to do is:
var Cat = Backbone.Model.extend({
defaults: {
name : 'Mr. Bigglesworth',
location : 'Living Room',
action : 'sleeping'
},
sleep: function () {
// POST /cats/{{ cat_id }}/action
// { action: "sleep" }
},
meow: function () {
// POST /cats/{{ cat_id }}/action
// { action: "meow" }
}
})
From what I can tell, the Backbone.Collection.save() method only performs the
following:
POST /cats/{{ cat_id }}
{ name: 'Mr. Bigglesworth', location: 'Living Room', action: '{{ value }} '}
But the API I'm working with won't let me change action that way, only by:
POST /cats/{{ cat_id }}/action
{ action: "{{ value }}" }
Hopefully that makes sense?
Any help would be appreciated.
You can pass the URL as a parameter when you call save. Maybe you can do something like this:
var Cat = Backbone.Model.extend({
urlRoot: '/cats/',
defaults: {
name : 'Mr. Bigglesworth',
location : 'Living Room',
action : 'sleeping'
},
sleep: function () {
var custom_url = this.urlRoot + this.id + "/action";
this.save({}, { url: custom_url});
// POST /cats/{{ cat_id }}/action
// { action: "sleep" }
},
});
See here: Posting form data using .save() to pass url parameters.
You can also implement the sync method to use another URL if you always want to use a custom URL on update. See for example here: backbone.js use different urls for model save and fetch.
There are different approaches you can take to solve this, but IMO the cleanest is to override Backbone.sync to act the way you want it to act if it's universal to the server backend you're connecting to.
For instance, if you want every one of your models/collections to interact with a particular backend implementation, this approach makes a lot of sense.
This way you can leave the rest of the Collection (or Model) code as the Backbone default but it will work the way you want it to work.
For example:
// Store the default Backbone.sync so it can be referenced later
Backbone.vanillaSync = Backbone.sync;
// Most of this is just copy-pasted from the original Backbone.sync
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
// Default options, unless specified.
_.defaults(options || (options = {}), {
emulateHTTP: Backbone.emulateHTTP,
emulateJSON: Backbone.emulateJSON
});
// Default JSON-request options.
var params = {type: type, dataType: 'json'};
// Ensure that we have a URL.
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
// START ADD YOUR LOGIC HERE TO ADD THE /action
// Add the action to the url
params.url = params.url + '/' + options.action;
// Remove the action from the options array so it isn't passed on
delete options.action;
// END ADD YOUR LOGIC HERE TO ADD THE /action
// Ensure that we have the appropriate request data.
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.data._method = type;
var beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
if (beforeSend) return beforeSend.apply(this, arguments);
};
}
// Don't process data on a non-GET request.
if (params.type !== 'GET' && !options.emulateJSON) {
params.processData = false;
}
// If we're sending a `PATCH` request, and we're in an old Internet Explorer
// that still has ActiveX enabled by default, override jQuery to use that
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
if (params.type === 'PATCH' && window.ActiveXObject &&
!(window.external && window.external.msActiveXFilteringEnabled)) {
params.xhr = function() {
return new ActiveXObject("Microsoft.XMLHTTP");
};
}
// Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
};
In the above example I assumed you had sent the action via the options array, if you actually wanted the static word /action you could just replace that block with:
// Add the action to the url
params.url = params.url + '/action';
This should give you the cleanest implementation while still keeping the rest of your code clean.