Ember JS: Customizing adapter to include multiple parameters - javascript

I currently have a database with 2 objects:
Role
Permission
ONE Role can have MANY permissions. I currently have my Role adapter setup as:
export default DS.RESTAdapter.extend(DataAdapterMixin, {
namespace: 'v1',
host: ENV.APP.API_HOST,
authorizer: 'authorizer:application',
pathForType: function(type) {
return 'staff/roles';
}
});
By default, when a Permission is added to a Role, it generates this request:
Request:
PUT /v1/staff/roles/1
Body:
{
"name": "name_of_role"
"permissions": [
{
"id": "3",
"name": "name_of_permission"
},
...
]
}
I'd like to customize my adapter to produce a request that looks like this instead:
Request:
PUT /v1/staff/roles/1/permissions/3
Body:
<None>
Can someone please tell me how I can go about doing this? Updating the server api to accommodate Ember JS is unfortunately not an option.
UPDATE:
Based on Ryan's response, here's a (I'll call it messy) workaround that did the trick for me.
Open to suggestions for making this more elegant:
export default DS.RESTAdapter.extend(DataAdapterMixin, {
namespace: 'v1',
host: ENV.APP.API_HOST,
authorizer: 'authorizer:application',
pathForType: function(type) {
return 'staff/roles';
},
updateRecord: function(embestore, type, snapshot) {
var roleID = snapshot.id;
var permissionID = snapshot.adapterOptions.permissionID;
var url = ENV.APP.API_HOST + "/v1/staff/roles/" + roleID + "/permissions/" + permissionID;
return new Ember.RSVP.Promise(function(resolve, reject){
Ember.$.ajax({
type: 'PUT',
url: url,
headers: {'Authorization': 'OAUTH_TOKEN'},
dataType: 'json',
}).then(function(data) {
Ember.run(null, resolve, data);
}, function(jqXHR) {
jqXHR.then = null; // tame jQuery's ill mannered promises
Ember.run(null, reject, jqXHR);
});
});
},
});

I can't find it in the Ember documentation but there is a universal ajax method attached to adapter that you can override.
So in my adapter to fit our auth scheme I've done this:
export default DS.RESTAdapter.extend({
host: ENV.host,
ajax: function(url, method, hash){
if(hash){
if(hash.data !== undefined && hash.data !== null){
hash.data.sessionId = this.getSessionId();
}
}else {
hash = {
data: {}
};
hash.data.sessionId = this.getSessionId();
}
return this._super(url, method, hash);
},
getSessionId: function(){
return window.sessionStorage.getItem('sessionId') || {};
}
}
This attaches the sessionId to every ajax call to the server made though out the entire application.
Changing it to modify your url based on the hash arguments passed in shouldn't be an issue.
My version of ember is 2.3.2 but I'm on the latest stable(2.5.2) version of ember-data and this is still working great in case you are worried about the age of that blog post I found.

Related

How to Login to MediaWiki (Wikipedia) API in Node.js

I'm trying the Wikipedia client login flow depicted in the API:Login docs, but something wrong happens:
1) I correctly get a token raised with the HTTP GET https://en.wikipedia.org/w/api.php?action=query&meta=tokens&type=login&format=json
and I get a valid logintoken string.
2.1) I then try the clientlogin like:
HTTP POST /w/api.php?action=clientlogin&format=json&lgname=xxxx&lgtoken=xxxx%2B%5C
and the POST BODY was
{
"lgpassword" : "xxxxx",
"lgtoken" : "xxxxx"
}
But I get an error:
{
"error": {
"code": "notoken",
"info": "The \"token\" parameter must be set."
},
"servedby": "mw1228"
}
If I try to change lgtoken to token I get the same result.
2.2) I have then tried the old method i.e. action=login and passing the body, but it does not work, since it gives me back another login token: HTTP POST https://en.wikipedia.org/w/api.php?action=login&format=json&lgname=xxxx
and the same POST BODY
I then get
{
"warnings": {}
},
"login": {
"result": "NeedToken",
"token": "xxxxx+\\"
}
where the docs here states that
NeedToken if the lgtoken parameter was not provided or no session was active (e.g. your cookie handling is broken).
but I have passed the lgtoken in the json body as showed.
I'm using Node.js and the built-in http module, that is supposed to pass and keep session Cookies in the right way (with other api it works ok).
I have found a similar issue on a the LrMediaWiki client here.
[UPDATE]
This is my current implementation:
Wikipedia.prototype.loginUser = function (username, password) {
var self = this;
return new Promise((resolve, reject) => {
var cookies = self.cookies({});
var headers = {
'Cookie': cookies.join(';'),
'Accept': '*/*',
'User-Agent': self.browser.userAgent()
};
// fetch login token
self.api.RequestGetP('/w/api.php', headers, {
action: 'query',
meta: 'tokens',
type: 'login',
format: 'json'
})
.then(response => { // success
if (response.query && response.query.tokens && response.query.tokens['logintoken']) {
self.login.logintoken = response.query.tokens['logintoken'];
self.logger.info("Wikipedia.login token:%s", self.login);
return self.api.RequestPostP('/w/api.php', headers, {
action: 'login',
format: 'json',
lgname: username
},
{
lgpassword: password,
lgtoken: self.login.logintoken
});
} else {
var error = new Error('no logintoken');
return reject(error);
}
})
.then(response => { // success
return resolve(response);
})
.catch(error => { // error
self.logger.error("Wikipedia.login error%s\n%#", error.message, error.stack);
return reject(error);
});
});
}//loginUser
where this.api is a simple wrapper of the Node.js http, the source code is available here and the api signatures are like:
Promise:API.RequestGetP(url,headers,querystring)
Promise:API.RequestPostP(url,headers,querystring,body)
If the currently accepted answer isn't working for someone, the following method will definitely work. I've used the axios library to send requests. Any library can be used but the key lies in formatting the body and headers correctly.
let url = "https://test.wikipedia.org/w/api.php";
let params = {
action: "query",
meta: "tokens",
type: "login",
format: "json"
};
axios.get(url, { params: params }).then(resp => {
let loginToken = resp.data.query.tokens.logintoken
let cookie = resp.headers["set-cookie"].join(';');
let body = {
action: 'login',
lgname: 'user_name',
lgpassword: 'password',
lgtoken: loginToken,
format: 'json'
}
let bodyData = new URLSearchParams(body).toString();
axios.post(url, bodyData, {
headers: {
Cookie: cookie,
}
}).then(resp => {
// You're now logged in!
// You'll have to add the following cookie in the headers again for any further requests that you might make
let cookie = resp.headers["set-cookie"].join(';')
console.log(resp.data)
})
})
And you should be seeing a response like
{
login: { result: 'Success', lguserid: 0000000, lgusername: 'Username' }
}
The second post request was where I got stuck for several hours, trying to figure out what was wrong. You need to send the data in an encoded form by using an API like URLSearchParams, or by just typing up the body as a string manually yourself.
I think from what you are saying you have lgtoken and lgname in the URL you are using, and then lgpassword and lgtoken (again!) in a JSON-encoded POST body.
This is not how the Mediawiki API works.
You submit it all as POST parameters. JSON is never involved, except when you ask for the result to come back in that format. I can't help you fix your code as you don't provide it, but that's what you need to do. (If you edit your question with your code, I'll do my best to help you.)
After seeing your code, I'll presume (without knowing the detail of your code) that you want something like this:
return self.api.RequestPostP('/w/api.php', headers, {
action: 'login',
format: 'json',
lgname: username,
lgpassword: password,
lgtoken: self.login.logintoken
});

Custom API call on Azure Mobile App Service

I have HTML/JS client trying to access APIController on Azure Mobile App service.
Following is my code
var _client = new WindowsAzure.MobileServiceClient("https://myapp.azurewebsites.net/");
var pp = _client.invokeApi("/Lookup/GetTransactionType", {
body: null,
method: "get",
parameters: { TenantID: 1 },
headers: {
"ZUMO-API-VERSION": "2.0.0",
"Content-Type":"application/json",
"Cache-Control":"false",
"x-zumo-auth": "tada"
}
}).done(function (results) {
var message = results.results.count;
}, function (error) {
alert(error.message)
});
The issue here is that my api's are published as such :
https://myapp.azurewebsites.net/Lookup/GetTransactionType?TenantID={{TenantID}}
But I get NOT FOUND error in client since its looking for following URL :
(XHR)GET - https://myapp.azurewebsites.net/api/Lookup/GetTransactionType?TenantID=1
How can I eliminate the /api in the URI?
As #rolspace mentioned, you need to call the .invokeApi function with the absolute URL (must start with http:// or https://) to eliminate the /api in the URI.
So you can change the line of code to:
var pp = _client.invokeApi(_client.applicationUrl + "/Lookup/GetTransactionType", { //...

Backbone - Declare Default Parameters for a Fetch inside a Model

I have some global parameters that I want to be sent in every time I call a fetch on a collection... my issue is I don't want to declare the data: { ... } every time I fetch.
Is there a way I can provide default parameters inside the Collection itself with the possibility to add more or override some?
For example:
Instead of doing this every time:
this.articlesCollection.fetch({
dataType: 'jsonp',
data: {
deviceType: GlobalVars.deviceType,
memberId: GlobalVars.memberId,
authToken: GlobalVars.authToken,
targetObjectId: userId,
limit: 50,
excludeArticleBodies: true,
excludeViewedItems: false
},
success: function() {
_this.render();
}
});
I'd like to just provide a one or two parameters and a success function, like this:
this.articlesCollection.fetch({
data: {
targetObjectId: userId
},
success: function() {
_this.render();
}
});
... and have the Collection look something like:
define([
'underscore',
'backbone',
'global',
'utilities',
'models/article/ArticleModel'
], function(_, Backbone, GlobalVars, Utils, ArticleModel){
var ArticlesCollection = Backbone.Collection.extend({
model: ArticleModel,
initialize : function(view) {
this.view = view;
},
dataType: 'jsonp',
data: {
deviceType: GlobalVars.deviceType,
memberId: GlobalVars.memberId,
authToken: GlobalVars.authToken,
limit: 50,
excludeArticleBodies: true,
excludeViewedItems: false
},
url : function() {
return GlobalVars.baseAPIUrl + '/API/GetArticles';
},
parse : function(data) {
return data.Articles;
}
});
return ArticlesCollection;
});
Here's a working jsFiddle with one approach: http://jsfiddle.net/LEuGq/1/
Basically, you configure both an object of defaultParams and params as properties of your collection, which are used to dynamically compute the correct URL when fetch() is called. This way is probably more in alignment with backbone than changing the API of fetch() to accept parameters, which it is not designed to do.
var ParamCollection = Backbone.Collection.extend({
defaultParams: {deviceType: 'raceCar', limit: 42},
params: {},
url: function() {
return "/paramcollection?" + $.param(_.defaults(this.params, this.defaultParams));
}
});
var paramCollection = new ParamCollection();
paramCollection.params.excludeArticleBodies = true;
paramCollection.params.limit = 52;
$("#debug").append(paramCollection.url());
Backbone uses jQuery's ajax call by default, so you can set up anything you need as a default using various methods. See this question for some examples: jQuery's ajaxSetup - I would like to add default data for GET requests only

Cross-domain Update from Sencha Touch 2 to external server

Trying to create a Sencha-Touch-2 app syncing to a Node.js server; code below.
The server uses another port on the same IP, so this is cross-domain.
(The server uses Mongoose to talk to a MongoDB back-end (not shown))
Using a JSONP Proxy as shown can read data from the server but breaks when writing:
"JSONP proxies can only be used to read data".
I guess the JSONP Proxy writer config is just to write the query and isn't used to write sync (save).
Sencha docs state an Ajax proxy can't go cross-domain, even though a
Cross-domain Ext.Ajax/Ext.data.Connection is discussed in the Sencha forums:
http://www.sencha.com/forum/showthread.php?17691-Cross-domain-Ext.Ajax-Ext.data.Connection
I have found several ways to do a (cross-domain) JSON post (e.g. Mobile Application Using Sencha Touch - JSON Request Generates Syntax Error)
but don't know how to integrate this as a writer in a proxy which syncs my store.
Sencha Touch: ScriptTagProxy url for create/update functionality
seems to offer pointers, but this is ajax and apparently unsuited for cross domain.
I've been reading this forum and elsewhere for a couple of days, but I seem to be stuck. Any help would be much appreciated.
Node.js and restify server
var server = restify.createServer({
name: 'Server',
key: fs.readFileSync(root+'/'+'privatekey.pem'),
certificate: fs.readFileSync(root+'/'+'certificate.pem')
});
server.use(restify.bodyParser());
server.use(restify.queryParser());
function getMessages(req, res, next) {
Model.find(function (err,data) {
res.setHeader('Content-Type', 'text/javascript;charset=UTF-8');
res.send(req.query["callback"] + '({"records":' + JSON.stringify(data) + '});');
});
}
function postMessage(req, res, next) {//not yet tested
var obj = new Model();
obj.name = req.params.name;
obj.description = req.params.description;
obj.date = new Date();
obj.save(function (err) {
if (err) throw err;
console.log('Saved.');
res.send('Saved.');
});
}
server.post(/^\/atapp/, postMessage);
server.get(/^\/atapp/, getMessages);
server.listen(port, ipaddr, function() {
console.log('%s: secure Node server started on %s:%d ...', Date(Date.now()), ipaddr, port);
});
Sencha Touch 2
Model
Ext.define('ATApp.model.User', {
extend: 'Ext.data.Model',
config: {
fields: [
{ name: 'name', type: 'string' },
{ name: 'description', type: 'string' },
{ name: 'date', type: 'date' },
{ name: '_id' }
...
Store
Ext.define('ATApp.store.Data', {
extend: 'Ext.data.Store',
requires: [
'ATApp.model.User',
'Ext.data.proxy.JsonP'
],
config: {
autoLoad: true,
model: 'ATApp.model.User',
storeId: 'Data',
proxy: {
type: 'jsonp',
model: 'ATApp.model.User',
url: 'https://192.168.2.45:13017/atapp',
reader: {
type: 'json',
idProperty: '_id',
rootProperty: 'records',
useSimpleAccessors: true
},
writer: {
type: 'json',
allowSingle: false,
encode: true,
idProperty: '_id',
rootProperty: 'records'
...
Controller
onNewDataRecord: function (view) {
console.log('newDataRecord');
var now = new Date();
var record = Ext.create('ATApp.model.User', {
date: now,
name: '..',
description: '..'
});
var store = Ext.data.StoreManager.lookup('Data')
record.setProxy(store.getProxy());
store.add(record);
this.activateEditor(record);
},
...
In Sencha-Touch-2 apps, the browser prohibits cross-domain AJAX calls (which violate the same-origin security policy). This pertains to different domains, different IP addresses and even different ports on the same IP address. JSONP circumvents this partly by fetching/reading data encapsulated in a script tag in a newly initiated HTTP GET message. In this way the Sencha-Touch-2 JSONP proxy can load a store (fetch/read) from a (cross domain) server. However, the JSONP proxy cannot write data. In 1 and 2 an approach is described which I have adapted.
My solution uses the JSONP proxy to fetch data, but not to store (which it can't). Instead, new records, and records to be saved or deleted are communicated with the server in a newly initiated HTTP GET message. Even though only HTTP GET is used, the server accepts message get (described in the question, above), put, del and new. Get is used by JSONP store/proxy load().
Node.js Server
//routes
server.get(/^\/atapp\/put/, putMessage);
server.get(/^\/atapp\/get/, getMessages);
server.get(/^\/atapp\/del/, delMessage);
server.get(/^\/atapp\/new/, newMessage);
function newMessage(req, res, next) {
var obj = new Model(); // Mongoose create new MongoDB object
obj.save(function (err,data) {
var x = err || data;
res.setHeader('Content-Type', 'text/javascript;charset=UTF-8');
res.send(req.query["callback"] + '({"payload":' + JSON.stringify(x) + '});');
}); // send reply to Sencha-Touch 2 callback
}
function putMessage(req, res, next) {
var q = JSON.parse(req.query.data); // no reply: add error recovery separately
var obj = Model.findByIdAndUpdate(q.key,q.val);
}
function delMessage(req, res, next) {
var key = JSON.parse(req.query.data);
Model.findByIdAndRemove(key); // no reply: add error recovery separately
}
Sencha Controller
New
onNewDataRecord: function (view) {
var control = this;
Ext.Ajax.Crossdomain.request({
url: 'https://192.168.2.45:13017/atapp/new',
rootProperty: 'payload',
scriptTag: true, // see [1](http://code.google.com/p/extjsdyntran/source/browse/trunk/extjsdyntran/WebContent/js/3rdparty/Ext.lib.Ajax.js?r=203)
success: function(r) { // process synchronously after response
var obj = r.payload;
var store = Ext.data.StoreManager.lookup('Data');
var key = obj._id // MongoDB document id
store.load(function(records, operation, success) { // add new record to store
var ind = store.findBy(function(rec,id) {
return rec.raw._id==key;
}); // identify record in store
var record = store.getAt(ind);
control.onEditDataRecord(view,record);
}, this);
}
});
Save
onSaveDataRecord: function (view, record) {
rec = {key:record.data.id, val: {}} // save template
var i; for (i in record.modified) rec.val[i]=record.data[i];
var delta = Ext.encode(rec); // only save modified fields
Ext.Ajax.Crossdomain.request({
url: 'https://192.168.2.45:13017/atapp/put',
params: {
data: delta,
},
rootProperty: 'payload',
scriptTag: true, // Use script tag transport
});
},
Delete
onDelDataRecord: function (view, record) {
var key = record.data.id;
Ext.Ajax.Crossdomain.request({ // delete document in db
url: 'https://192.168.2.45:13017/atapp/del',
params: {
data: Ext.encode(key),
format: 'json'
},
rootProperty: 'payload',
scriptTag: true, // Use script tag transport
});
record.destroy(); // delete record from store
},

Default $resource POST data

That might be strange but I need to specify some default POST data for my $resource using the factory method of the module.
Does anyone have an idea of how to do that in AngularJS ?
EDIT :
Well, i want to do something like this :
/**
* Module declaration.
* #type {Object}
*/
var services = angular.module("services", ["ngResource"]);
/**
* Product handler service
*/
services.factory("Product", function($resource) {
return $resource("http://someUrl", {}, {
get : {method: "GET", params: {productId: "-1"}},
update: {method : "POST", params:{}, data: {someDataKey: someDataValue}}
});
});
Where data is the default data for my future POST requests.
This is not really the angular way to do such a thing as you lose data consistency if you do it and it doesn't reflect in your model.
Why?
The resource factory creates the object and uses object instance data as POST. I have looked at the documentation and angular-resource.js and there doesn't seem to be a way to specify any default custom properties for the object being created by resource without modifying angular-resource.js.
What you can do is:
services.factory("Product", function($resource) {
return $resource("http://someUrl", {}, {
get : {method: "GET", params: {productId: "-1"}},
update: {method : "POST"}
});
});
and in your controller:
$scope.product = {}; // your product data initialization stuff
$scope.product.someDataKey = 'someDataValue'; // add your default data
var product = new Product($scope.product);
product.$update();
I think it will depend on how you call the update function. If you read the angular main page's tutorial, under "Wire up a Backend", the mongolab.js provides a 'Project' factory. Copied verbatim:
angular.module('mongolab', ['ngResource']).
factory('Project', function($resource) {
var Project = $resource('https://api.mongolab.com/api/1/databases' +
'/angularjs/collections/projects/:id',
{ apiKey: '4f847ad3e4b08a2eed5f3b54' }, {
update: { method: 'PUT' }
}
);
Project.prototype.update = function(cb) {
return Project.update({id: this._id.$oid},
angular.extend({}, this, {_id:undefined}), cb);
};
Project.prototype.destroy = function(cb) {
return Project.remove({id: this._id.$oid}, cb);
};
return Project;
});
The usage is that you first get an instance of the Project:
project = Project.get({id:1});
Then do an update after some changes:
project.update(someFunction);
In your case, you can change the update to always add the data you need:
Product.prototype.update = function(cb) {
return Product.update({},
angular.extend({}, this, {someDataKey: someDataValue}), cb);
};
Otherwise, you can most likely put the key/value pair in the params:
update: {method : "POST", params:{someDataKey: someDataValue}}
It will be POSTed with the key/value pair in the URL, but most app servers nowadays will throw the pair into the params object anyway.
I think most have missed a tiny gem in the documentation here.
non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
This suggests you can do the following.
var User = $resource('/user');
postData = { name : 'Sunil', 'surname' : 'Shantha' };
var user = User.save({notify:'true'}, postData, function() {
// success!
});
The second parameter when doing a save action (post) is post data.
Wrapper function will work.
function myPost(data) {
return $http.post('http://google.com', angular.extend({default: 'value'}, data))
}
myPost().success(function(response) { ... });
Might this solve your problem?
services.factory("Product", function($resource) {
return $resource("http://someUrl", {}, {
get : {method: "GET", params: {productId: "-1"}},
update: {method : "POST", params:{}, data: {someDataKey: someDataValue}}
});
});
services.factory("DefaultProduct", function(Product) {
return function(){
return new Product({
data:"default";
});
};
});
services.controller("ProductCTRL",function($scope,DefaultProduct){
$scope.product = new DefaultProduct();
});
You can just merge your params with the default. Everything not available in params will be provided by the default object. Everything available will be overwritten by myParams
services.factory("Product", function($resource) {
return $resource("http://someUrl", {}, {
get : {method: "GET", params: {productId: "-1"}},
update: {method : "POST", params:angular.extend(myDefault, myParams);}
});
});
where myParams would be your list of variables and myDefault your default values as a json object.
You can set default fields on your request by using transformRequest option for your $resource's actions that use the POST method.
For example something like this
function prependTransform(defaults, transform) {
// We can't guarantee that the default transformation is an array
defaults = angular.isArray(defaults) ? defaults : [defaults];
// Append the new transformation to the defaults
return [transform].concat(defaults);
}
ctrl.factory('MyResource', ['$resource', '$http',
function($resource, $http) {
return $resource('/path/to/myresource/:id', {id : '#id'},
{
create : {
method : 'POST',
transformRequest : prependTransform($http.defaults.transformRequest,
function(data, headers) {
return addDefaultField(data);
}
),
},
});
}
]);

Categories