Ember computed property depending on service property not updating - ember.js

In my Ember 2.8 application, I'm establishing a Websocket connection in a service. The connection URL changes when a user is logged in (it then includes the user auth token as a query parameter).
The current user service is simple:
CurrentUserService = Ember.Service.extend(
name: "current-user"
user: null
load: ->
// Do some stuff
#set("user", user)
)
It works exactly as expected, and I use it to display the current users username on the page (among other things).
In the Websocket service, all I do is create a computed property, depending on currentUser.user, that sets up the connection (depending on whether a user is logged in):
ActionCableService = Ember.Service.extend(
name: "action-cable"
cable: service()
currentUser: service()
testObs: Ember.observer("currentUser", ->
console.log "currentUser changed, #{ #get("currentUser.user") }"
)
consumer: Ember.computed("currentUser.user", ->
consumerUrl = "ws://localhost:10000/cable"
if #get("currentUser").user?
consumerUrl += "?token=#{ #get("currentUser.user.authToken") }"
console.log(consumerUrl)
return #get("cable").createConsumer(consumerUrl)
)
)
Problem is, the consumer property never gets updated. It's set once, on page load, and when the user property of the currentUser service changes, consumer is not updated, and neither does my test observer.
When I refresh the page, sometimes the logged in consumerUrl is used, and sometimes it's not.
I'm guessing sometimes the session restoration happens first, and sometimes the action cable service happens first.
What I expected to happen when the action cable service gets loaded first is:
Action cable service gets loaded, no current user set yet, connect to public websocket
Logic that handles restoring user from session data fires, sets currentUser.user (this happens, I can see the username on my page)
The consumer computed property notices the currentUser.user change and connects to the private consumerUrl (does not happen)
I can very easily solve this problem in a way that does not depend on computed properties, but I would like to know what went wrong here.

Computed properties, by default, observe any changes made to the properties they depend on, and are dynamically updated when they're called.
Unless you are invoking or calling that computed property, it will not execute your intended code.
Observers, on the other hand, react without invocation, when the property they are watching, changes. But they are often overused, and can easily introduce bugs due to their synchronous nature.
You could refactor your observers and computed properties into helper functions that are called directly. This makes them easier to unit test as well.
In your controller, you can handle the initial action of logging in, like this:
currentUser: Ember.inject.service(),
actions: {
login() {
this.auth({ username: 'Mary' });
},
},
auth(data) {
// Send data to server for authentication...
// ...upon response, handle the following within the promise's `then`
// method, failures caught within `catch`, etc. But for purposes of
// demonstration, just mocking this for now...
const response = { username: 'Mary', authToken: 'xyz', };
this.get('currentUser').setConsumer(response);
},
The current-user service could then set it’s properties, and call a helper function on the action-cable service:
actionCable: Ember.inject.service(),
authToken: null,
username: null,
setConsumer(response) {
this.set('authToken', response.authToken);
this.set('username', response.username);
this.get('actionCable').setConsumer();
},
The action-cable service reads properties from currentService, sets the consumerUrl, and calls the cable service to create the consumer:
cable: Ember.inject.service(),
currentUser: Ember.inject.service(),
setConsumer() {
var consumerUrl = "ws://localhost:10000/cable";
if (this.get("currentUser.username") !== null) {
consumerUrl += "?token=" + (this.get("currentUser.authToken"));
}
console.log("ACTION CABLE SERVICE, Consumer URL: ", consumerUrl);
this.get("cable").createConsumer(consumerUrl);
}
I’ve created an Ember Twiddle to demonstrate.

Another way would be to emit an event in the service and then subscribe to the event in the init method and set the value of the dependent key of the computed property to force it to be recomputed/updated.

Related

Redux: What is the correct place to save cookie after login request?

I have the following situation: The user enters his credentials and clicks a Login button. An API call is done in the action creator via redux-thunk. When the API call was successful, another action is dispatched containing the response from the server. After the (successful) login I want to store the users session id in a cookie (via react-cookie).
Action creator
export function initiateLoginRequest(username, password) {
return function(dispatch) {
dispatch(loginRequestStarting())
return fetch('http://path.to/api/v1/login',
{
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
})
.then(checkStatus)
.then(parseJSON)
.then(function(data) {
dispatch(loginRequestSuccess(data))
})
.catch(function(error) {
dispatch(loginRequestError(error))
})
}
}
export function loginRequestSuccess(user) {
return {
type: ActionTypes.LOGIN_REQUEST_SUCCESS,
user
}
}
Reducer
export default function user(state = initialState, action) {
switch (action.type) {
case ActionTypes.LOGIN_REQUEST_SUCCESS:
cookie.save('sessionId', action.user.sid, { path: '/' })
return merge({}, state, {
sessionId: action.user.sid,
id: action.user.id,
firstName: action.user.first_name,
lastName: action.user.last_name,
isAuthenticated: true
})
default:
return state
}
}
Right now the reducer responsible for LOGIN_REQUEST_SUCCESS saves the cookie. I know the reducer has to be a pure function.
Is saving a cookie in the reducer violating this principle? Would it be better to save the cookie inside the action creator?
Have a look at redux-persist.
You can persist/save your reducers (or parts of them) in LocalStorage.
Concept
Initiate login.
Receive cookie from server.
Dispatch login success.
Reducer stores cookie in memory.
Persist middleware stores reducer state in LocalStorage.
Example
Install
npm install --save-dev redux-persist
Example Usage
Create a component that wraps the persistence/rehydration logic.
AppProvider.js
import React, { Component, PropTypes } from 'react';
import { Provider } from 'react-redux';
import { persistStore } from 'redux-persist';
class AppProvider extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
children: PropTypes.node
}
constructor(props) {
super(props);
this.state = { rehydrated: false };
}
componentWillMount() {
const opts = {
whitelist: ['user'] // <-- Your auth/user reducer storing the cookie
};
persistStore(this.props.store, opts, () => {
this.setState({ rehydrated: true });
});
}
render() {
if (!this.state.rehydrated) {
return null;
}
return (
<Provider store={this.props.store}>
{this.props.children}
</Provider>
);
}
}
AppProvider.propTypes = {
store: PropTypes.object.isRequired,
children: PropTypes.node
}
export default AppProvider;
Then, in your index.js or file in which you set up the store, wrap the rendered components in your new AppProvider.
index.js
...
import AppProvider from 'containers/AppProvider.jsx';
...
render((
<AppProvider store={store}>
...
</AppProvider>
), document.getElementById('App'));
This will serialize your user reducer state to LocalStorage on each update of the store/state. You can open your dev tools (Chrome) and look at Resources => Local Storage.
I'm not sure if this is the "right" way, but that's how my team is persisting the logged user in the Redux app we built:
We have a very default architecture, an API ready to receive requests in one side, and a React/Redux/Single Page app that consumes this API endpoints in the other side.
When the user credentials are valid, the API's endpoint responsible for the login respond to the app with the user object, including an access token. The access token is latter used in every request made by the app to validate the user against the API.
When the app receives this user information from the API two things happen: 1) an action is dispatched to the users reducer, something like ADD_USER, to include this user in the users store and 2) the user's access token is persisted in the localStorage.
After this, any component can connect to the users reducer and use the persisted access token to know who is the logged user, and of course if you have no access token in your localStorage it means the user is not logged in.
In the top of our components hierarchy, we have one component responsible to connect to the users reducer, get the current user based on the access token persisted in the localStorage, and pass this current user in the React's context. So we avoid every component that depends on the current user to have to connect to the users reducer and read from the localStorage, we assume that this components will always receive the current user from the app's context.
There are some challenges like token expiration that adds more complexity to the solution, but basically this is how we are doing it and it's working pretty well.
I'd probably have the server-side set the cookie, personally, and make it transparent to JavaScript. But if you really want to do it client-side, I'd do it in an action helper. Something like this:
// Using redux-thunk
function login(user, password) {
return dispatch => api.auth.login(user, password)
.then(result => setCookie())
.then(() => dispatch({type: 'USER_LOGGED_IN'}))
}
Or something like that.
Action helpers don't need to be pure, but reducers should be. So, if I'm doing side-effects, I put them into action helpers.

Ember-CLI-Mirage enforcing JSON:API?

Stumped on a couple failures and want to know if I'm understanding Mirage correctly:
1.In ember-cli-mirage, am I correct that the server response I define should reflect what my actual server is returning? For example:
this.get('/athletes', function(db, request) {
let athletes = db.athletes || [];
return {
athletes: athletes,
meta: { count: athletes.length }
}
});
I am using custom serializers and the above matches the format of my server response for a get request on this route, however, on two tests I'm getting two failures with this error: normalizeResponse must return a valid JSON API document: meta must be an object
2.Is mirage enforcing the json:api format, and is it doing so because of the way I'm setting up the tests?
For example, I have several tests that visit the above /athletes route, yet my failures occur when I use an async call like below. I would love to know the appropriate way to correctly overwrite the server response behavior, as well as why the normalizeResponse error appears in the console for 2 tests but only causes the one below to fail.
test('contact params not sent with request after clicking .showglobal', function(assert) {
assert.expect(2);
let done = assert.async();
server.createList('athlete', 10);
//perform a search, which shows all 10 athletes
visit('/athletes');
fillIn('.search-inner input', "c");
andThen(() => {
server.get('/athletes', (db, request) => {
assert.notOk(params.hasOwnProperty("contacts"));
done();
});
//get global athletes, which I thought would now be intercepted by the server.get call defined within the andThen block
click('button.showglobal');
});
});
Result:
✘ Error: Assertion Failed: normalizeResponse must return a valid JSON API document:
* meta must be an object
expected true
I tried changing my server response to a json:api format as suggested in the last example here but this looks nothing like my actual server response and causes my tests to fail since my app doesn't parse a payload with this structure. Any tips or advice must appreciated.
You are correct. Are the failures happening for the mock you've shown above? It looks to me like that would always return meta as an object, so verify the response is what you think it should be by looking in the console after the request is made.
If you'd like to see responses during a test, enter server.logging = true in your test:
test('I can view the photos', function() {
server.logging = true;
server.createList('photo', 10);
visit('/');
andThen(function() {
equal( find('img').length, 10 );
});
});
No, Mirage is agnostic about your particular backend, though it does come with some defaults. Again I would try enabling server.logging here to debug your tests.
Also, when writing asserts against the mock server, define the route handlers at the beginning of the test, as shown in the example from the docs.
I was able to get my second test to pass based on Sam's advice. My confusion was how to assert against the request params for a route that I have to visit and perform actions on. I was having to visit /athletes, click on different buttons, and each of these actions was sending separate requests (and params) to the /athletes route. That's is why I was trying to redefine the route handler within the andThen block (i.e. after I had already visited the route using the route definition in my mirage/config file).
Not in love with my solution, but the way I handled it was to move my assertion out of route handler and instead assign the value of the request to a top-level variable. That way, in my final andThen() block, I was able to assert against the last call to the /athletes route.
assert.expect(1);
//will get assigned the value of 'request' on each server call
let athletesRequest;
//override server response defined in mirage/config in order to
//capture and assert against request/response after user actions
server.get('athletes', (db, request) => {
let athletes = db.athletes || [];
athletesRequest = request;
return {
athletes: athletes,
meta: { count: athletes.length }
};
});
//sends request to /athletes
visit('/athletes');
andThen(() => {
//sends request to /athletes
fillIn('.search-inner input', "ab");
andThen(function() {
//sends (final) request to /athletes
click('button.search');
andThen(function() {
//asserts against /athletes request made on click('button.search') assert.notOk(athletesRequest.queryParams.hasOwnProperty("contact"));
});
});
});
I'm still getting console errors related to meta is not an object, but they are not preventing tests from passing. Using the server.logging = true allowed me to see that meta is indeed an object in all FakeServer responses.
Thanks again to Sam for the advice. server.logging = true and pauseTest() make acceptance tests a lot easier to troubleshoot.

How to successfully instantiate a db model in a Sails test

I have been following the Sails.js documentation for testing, here:
http://sailsjs.org/documentation/concepts/testing
I have successfully been able to implement Controller tests that
hit different paths of my app, and check the responses of different Express requests.
My trouble is knowing A) How to instantiate a Model, specifically from my User model B) How I can guarantee that the model is successfully created.
I currently have a test in which, in a before hook, I create new user with all the required attributes:
before(function(){
User.create({firstName:"Bob", lastName: "Balaban", password:"12345", email:"bob#bob.com"})
});
The problem is that, I do not know how to verify if this record has been added to my tests database, or if a validation error or some other error is thrown upon the call to create.
NOTE: I ask this, because a test which is dependent upon the before() hook successfully functioning fails, and the only reason it could possible fail is if the User wasn't actually added to the db
You need to wait for the User to be created in before by using the done callback function argument, and calling it after you're done with setting up the test environment. Also, you're not lifting sails here for some reason despite the docs urging you to do so. I'd also recommend using a test database instead of your normal database so your test data is independent of your production / development data.
Example code below. The addition of done and the exec callback are probably the most vital parts.
var Sails = require('sails'), sails;
// ...
before(function(done) {
// Increase the Mocha timeout so that Sails has enough time to lift.
this.timeout(10000);
Sails.lift({
// If you want to use a different DB for testing, uncomment these and replace with your own DB info.
/*connections: {
// Replace the following with whatever suits you.
testMysql: {
adapter : 'sails-mysql',
host : 'localhost',
port : 3306,
user : 'mySQLUser',
password : 'MyAwesomePassword',
database : 'testDB'
}
},
models: {
connection: 'testMysql',
migrate: 'drop'
}
*/
}, function(err, server) {
sails = server;
if (err) return done(err);
User.create({firstName:"Bob", lastName: "Balaban", password:"12345", email:"bob#bob.com"})
.exec(function(err, createdUser) {
if (err) {
console.log("Failed to create user! Error below:");
console.log(err);
}
else {
console.log("User created successfully:");
console.log(user);
}
done(err, sails);
})
});
});

Get Ember-Data REST response value

Ember: 1.0.0-rc.6
Ember-Data: e999edb (2013-07-06 06:03:59 -0700)
I make a REST call (POST) to login a user.
Server response is ok.
I need the ID from the server, but i only got the ID with "setTimeout".
I think this is not the right way.
What is my mistake?
Within Controller i call:
var login = App.Login.createRecord(this.getProperties("email", "password"));
login.on("didCreate", function(record) {
console.log(record.get("id")); // ID is null
console.log(record.get("email"));
});
setTimeout(function() {
console.log(login.get("id")); // ID is available
console.log(login.get("email"));
}, 500);
DS.defaultStore.commit();
You're right -- there's a bug in ember-data where the materializeData event, which basically sets the id and unwraps the server response doesn't happen until AFTER the didCreate callback. So what's happening is that in your login.on("didCreate" ....) callback, the record still hasn't materialized yet.
This seems to still be an issue -- see this thread for more information: https://github.com/emberjs/data/issues/405#issuecomment-17107045
Workaround
Your work around is fine, but an easier (cleaner?) one is to wrap your callback actions in a Ember.run.next:
login.on("didCreate", function(record) {
Ember.run.next(function() {
console.log(record.get("id")); // ID should be set
console.log(record.get("email"));
}
});
This way, at least you don't need to deal with the timeout.
I believe this works by delaying the actions until the next run loop, and by then the materialization should have already happened. More on the Ember run loop
Source: https://github.com/emberjs/data/issues/405#issuecomment-18726035

emberjs handle 401 not authorized

I am building an ember.js application and am hung up on authentication. The json rest backend is rails. Every request is authenticated using a session cookie (warden).
When a user first navigates to the application root rails redirects to a login page. Once the session is authorized the ember.js app is loaded. Once loaded the ember.js app makes requests to the backend using ember-data RESTadapter and the session for authorization.
The problem is the session will expire after a predetermined amount of time. Many times when this happens the ember.js app is still loaded. So all requests to the backend return a 401 {not autorized} response.
To fix this problem I am thinking the ember.js app needs to notify the user with a login modal every time a 401 {not autorized} response is returned from the server.
Does anyone know how to listen for a 401 {not autorized} response and allow the user to re-login without losing any changes or state.
I have seen other approaches such as token authorization but I am concerned with the security implications.
Anybody have a working solution to this problem?
As of the current version of Ember Data (1.0 beta) you can override the ajaxError method of DS.RESTAdapter:
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var error = this._super(jqXHR);
if (jqXHR && jqXHR.status === 401) {
#handle the 401 error
}
return error;
}
});
Note that you should call #_super, especially if you are overriding one of the more complex adapters like DS.ActiveModelAdapter, which handles 422 Unprocessable Entity.
AFAIK this is not addressed by the current implementation of ember-data and the ember-data README states that "Handle error states" is on the Roadmap.
For the time being, you can implement your own error handling adapter. Take a look at the implementation of the DS.RestAdapter . By using that as a starter, it should not be too difficult to add error handling in there (e.g simply add an error function to the the data hash that is passed to the jQuery.ajax call).
For those willing to accept a solution that does lose changes and state you can register a jQuery ajaxError handler to redirect to a login page.
$(document).ajaxError(function(event, jqXHR, ajaxSettings, thrownError) {
// You should include additional conditions to the if statement so that this
// only triggers when you're absolutely certain it should
if (jqXHR.status === 401) {
document.location.href = '/users/sign_in';
}
});
This code will get triggered anytime any jQuery ajax request completes with an error.
Of course you would never actually use such a solution as it creates an incredibly poor user experience. The user is yanked away from what they're doing and they lose all state. What you'd really do is render a LoginView, probably inside of a modal.
An additional nicety of this solution is that it works even if you occasionally make requests to your server outside of ember-data. The danger is if jQuery is being used to load data from other sources or if you already have some 401 error handling built-in elsewhere. You'll want to add appropriate conditions to the if statement above to ensure things are triggered only when you're absolutely certain they should.
It's not addressed by ember-data (and probably won't be), but you can reopen the DS class and extend the ajax method.
It looks like this:
ajax: function(url, type, hash) {
hash.url = url;
hash.type = type;
hash.dataType = 'json';
hash.contentType = 'application/json; charset=utf-8';
hash.context = this;
if (hash.data && type !== 'GET') {
hash.data = JSON.stringify(hash.data);
}
jQuery.ajax(hash);
},
You can rewrite it with something like this (disclaimer: untested, probably won't work):
DS.reopen({
ajax: function(url, type, hash) {
var originalError = hash.error;
hash.error = function(xhr) {
if (xhr.status == 401) {
var payload = JSON.parse(xhr.responseText);
//Check for your API's errorCode, if applicable, or just remove this conditional entirely
if (payload.errorCode === 'USER_LOGIN_REQUIRED') {
//Show your login modal here
App.YourModal.create({
//Your modal's callback will process the original call
callback: function() {
hash.error = originalError;
DS.ajax(url, type, hash);
}
}).show();
return;
}
}
originalError.call(hash.context, xhr);
};
//Let ember-data's ajax method handle the call
this._super(url, type, hash);
}
});
What we're doing here is essentially deferring the call that received the 401 and are preserving the request to be called again when login is complete. The modal's ajax call with have the original error applied to it from the original ajax call's hash, so the original error would still work as long as it's defined :-)
This is a modified implementation of something we're using with our own data-persistence library, so your implementation might vary a bit, but the same concept should work for ember-data.