How to reload hasMany relationship in ember-data with ember octane? - ember.js

After upgrading to Ember 3.21.2 from 3.9, reloading hasMany relationships is not working properly anymore. For example the following model hook to fetch editable contents for a user does not update the user model anymore.
model(params) {
const { user } = this.modelFor('application')
const requestParams = this.mapParams(params)
return RSVP.hash({
user,
results: user.hasMany('editableContents').reload({
adapterOptions: requestParams
})
})
},
It still triggers requests, but it loads the same contents with every request, even after the request params have changed. Initially the request is sent to /users/:user_id/editable-contents?filter=......
After changing the adapter options it sends a request for each content to /contents/:content_id
We believe the .reload() function is to blame, because we found out that the .hasMany('editableContents').reload() does not jump to the findHasMany() hook in our application adapter, but instead calls findRecord() for each record.
We are using:
"ember-cli": "~3.21.2",
"ember-data": "~3.21.0"
Any help is appreciated. Thanks!

With the help of user sly7-7 on Ember's Discord server we got pointed in the right direction.
The real problem was that our payload was missing the "related" link, because a paginated payload does not contain that link. A missing related link wasn't a problem before a change in ember-data that implemented the following block that will never be called if payload.links.related is undefined:
if (payload.links) {
let originalLinks = this.links;
this.updateLinks(payload.links);
if (payload.links.related) {
let relatedLink = _normalizeLink(payload.links.related);
let currentLink = originalLinks && originalLinks.related ? _normalizeLink(originalLinks.related) : null;
...
}
see: https://github.com/emberjs/data/blob/ff4f9111fcfa7dd9e39804ed17f5af27a4a01378/packages/record-data/addon/-private/relationships/state/relationship.ts#L633
As a workaround we overrode the normalizeArrayResponse() hook in our application serializer and set the related link to the base request link if there is no related link:
normalizeArrayResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.links && payload.links.first && !payload.links.related) {
const baseLink = payload.links.first.split('?')[0]
if (isRelationshipLink(baseLink)) {
payload.links.related = baseLink
}
}
}
With that workaround, which also uses a rather hacky way to see if the link is a relationship link, the .reload() function works again globally in our application.
To be safe we will also see if we can send the related links with the backend response, which would be cleaner than above workaround.

Related

Ember Super Rentals Tutorial 3.15 - Working with data

I was following the ember Super Rental 3.15 tutorial, when I got to the working with data section, I updated the route index file with model hooks, the page stopped working. Also I am finding ember tutorials to be incomplete.
error says property of map is undefined
code in routes index.js file:
import Route from '#ember/routing/route';
const COMMUNITY_CATEGORIES = [
'Condo',
'Townhouse',
'Apartment'
];
export default class IndexRoute extends Route {
async model() {
let response = await fetch('/api/rentals.json');
let { data } = await response.json();
return data.map(model => {
let { attributes } = model;
let type;
if (COMMUNITY_CATEGORIES.includes(attributes.category)) {
type = 'Community';
} else {
type = 'Standalone';
}
return { type, ...attributes };
});
}
}
image if error message:
Your problem is that fetch('/api/rentals.json'); does not return the correct data. And so when you do let { data } = await response.json(); then data will be undefined and you can not do undefined.map.
So the code you posted is correct. The problem is somewhere else. You can check:
did you correctly add the rentals.json file? If you open http://localhost:4200/api/rentals.json do you see the data? So have you done this?
I see some error from mirage. The super-rentals tutorial does not use mirage. I can see this here (sidenote: that git repo is automatically created from the guides, so its always up to date). So this could be your problem. Depending how you configure mirage it will basically mock all your ajax requests. This means that fetch(... will no longer work then expected, mirage assumes you always want to use mocked data and you did not configure mirage correctly. You can try to remove mirage from your package.json, rerun npm install, restart the ember server and try it again.

Reload model/update template on createRecord save

I see this question is being ask all over again still don't find solution that works for such a trivial task.
This url displays a list of navigations tabs for workspaces.
http://localhost:4200/users/1/workspaces
Each of tab resolves to
http://localhost:4200/users/1/workspaces/:wid
Also on the I have a button that suppose to create a new workspace as well as new tab.
Here how controller for looks:
export default Ember.Controller.extend({
actions: {
newWorkspace: function () {
this.get('currentModel').reload();
var self = this;
var onFail = function() {
// deal with the failure here
};
var onSuccess = function(workspace) {
self.transitionToRoute('dashboard.workspaces.workspace', workspace.id);
};
this.store.createRecord('workspace', {
title: 'Rails is Omakase'
}).save().then(onSuccess, onFail);
}
}
});
When I click on button I see in ember inspector new record indeed created as well as url redirected to id that represents newly created workspace.
My question is how to force model/template to reload. I have already killed 5h trying model.reload() etc. Everything seem not supported no longer. Please please help.
UPDATE
When adding onSuccess
model.pushObject(post);
throws Uncaught TypeError: internalModel.getRecord is not a function
I believe you should call this.store.find('workspace', workspace.id) for Ember Data 1.12.x or earlier. For 1.13 and 2.0 there are more complicated hooks that determine whether or not the browser should query the server again or use a cached value; in that case, call this.store.findRecord('workspace', workspace.id, { reload: true }).
I do not know if this help. I had a similar problem. My action was performed in the route. Refresh function took care of everything.

Adding headers after RESTAdapter initialization

I am trying to add an Authorization header to my adapter's request after the adapter has been initialized and used. I can add headers in a static way at the time I create my ApplicationAdapter, but I can't seem to get it use the headers in subsequent REST calls. I am trying this:
var auth= "Basic " + hash;
App.ApplicationAdapter.reopen({
headers: {
Authorization: auth
}
});
I have debugged RESTAdapter in the ajax method, and the test for adapter.headers is always undefined.
The accepted answer doesn't address the fact that the recommended approach is not working in ember-data. I say recommended since:
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L88
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L162
and other places in that file.
Further, the issue the OP brings up with of undefined specifically happens here:
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js#L619
So, the following simply does not work:
App.ApplicationAdapter.reopen({
headers: {token: 'reopen_token (NO WORK)' }
});
I've tried to point to this out as an issue but it got closed within an hour:
https://github.com/emberjs/data/issues/1820
Hopefully core will decide to either fix this or remove the comments. But, yes, for now it seems you have to hijack jQuery ajax setup, Ember.$.ajaxPrefilter, or override the ajax on the adapter yourself.
EDIT: So after getting some more feedback from Ember devs, it looks like the core of this issue is trying to reopen an instance already created. So using a computered property when it's defined (so it will update as desired) seems to be the advised approach. Hope that helps (there's a recently merged pull request that makes this more obvious in the comments of referenced file:https://github.com/emberjs/data/pull/1818/files#diff-1d7f5a5b77898df15de501c3c38d4829R108 )
EDIT 2: Got this working in my app so here's the code in case someone else gets stuck:
//app.js
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
namespace: 'api/v1',
headers: function() {
return {
token: this.get('App.authToken') || localStorage.getItem('token')
};
}.property("App.authToken")
});
//login-controller.js (only action shown..assume `data` has user/pass)
actions: {
login: function() {
$.post('/token/', data).done(function(user) {
App.set('authToken', user.token);
//Above will trigger adapters's header computed property to update
// Transition to previous attempted route
var attemptedTransition = self.get('attemptedTransition');
if(attemptedTransition) {
attemptedTransition.retry();
}
else {
self.transitionToRoute('yourapproute');
}
})
.fail(function(response) {
//fail handling omitted
});
The answers are already introduced in official API document.
http://emberjs.com/api/data/classes/DS.RESTAdapter.html#toc_headers-customization
Use computed property with session injection
or just use volatile computed property
You should be able to use $.ajaxPrefilter to add custom headers (or params).
See: http://api.jquery.com/jQuery.ajaxPrefilter/
Ember.$.ajaxPrefilter(function( options, oriOptions, jqXHR ) {
var auth= "Basic " + hash;
jqXHR.setRequestHeader("Authorization", auth);
});

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.

How should errors be handled when using the Ember.js Data RESTAdapter?

ember-data.js: https://github.com/emberjs/data/tree/0396411e39df96c8506de3182c81414c1d0eb981
In short, when there is an error, I want to display error messages in the view, and then the user can 1) cancel, which will rollback the transaction 2) correct the input errors and successfully commit the transaction, passing the validations on the server.
Below is a code snippet from the source. It doesn't include an error callback.
updateRecord: function(store, type, record) {
var id = get(record, 'id');
var root = this.rootForType(type);
var data = {};
data[root] = this.toJSON(record);
this.ajax(this.buildURL(root, id), "PUT", {
data: data,
context: this,
success: function(json) {
this.didUpdateRecord(store, type, record, json);
}
});
},
Overall, what is the flow of receiving an error from the server and updating the view? It seems that an error callback should put the model in an isError state, and then the view can display the appropriate messages. Also, the transaction should stay dirty. That way, the transaction can use rollback.
It seems that using store.recordWasInvalid is going in the right direction, though.
This weekend I was trying to figure the same thing out. Going off what Luke said, I took a closer look at the ember-data source for the latest commit (Dec 11).
TLDR; to handle ember-data update/create errors, simply define becameError() and becameInvalid(errors) on your DS.Model instance. The cascade triggered by the RESTadapter's AJAX error callback will eventually call these functions you define.
Example:
App.Post = DS.Model.extend
title: DS.attr "string"
body: DS.attr "string"
becameError: ->
# handle error case here
alert 'there was an error!'
becameInvalid: (errors) ->
# record was invalid
alert "Record was invalid because: #{errors}"
Here's the full walk through the source:
In the REST adapter, the AJAX callback error function is given here:
this.ajax(this.buildURL(root, id), "PUT", {
data: data,
context: this,
success: function(json) {
Ember.run(this, function(){
this.didUpdateRecord(store, type, record, json);
});
},
error: function(xhr) {
this.didError(store, type, record, xhr);
}
});
didError is defined here and it in turn calls the store's recordWasInvalid or recordWasError depending on the response:
didError: function(store, type, record, xhr) {
if (xhr.status === 422) {
var data = JSON.parse(xhr.responseText);
store.recordWasInvalid(record, data['errors']);
} else {
store.recordWasError(record);
}
},
In turn, store.recordWasInvalid and store.recordWasError (defined here) call the record (a DS.Model)'s handlers. In the invalid case, it passes along error messages from the adapter as an argument.
recordWasInvalid: function(record, errors) {
record.adapterDidInvalidate(errors);
},
recordWasError: function(record) {
record.adapterDidError();
},
DS.Model.adapterDidInvalidate and adapterDidError (defined here) simply send('becameInvalid', errors) or send('becameError') which finally leads us to the handlers here:
didLoad: Ember.K,
didUpdate: Ember.K,
didCreate: Ember.K,
didDelete: Ember.K,
becameInvalid: Ember.K,
becameError: Ember.K,
(Ember.K is just a dummy function for returning this. See here)
So, the conclusion is, you simply need to define functions for becameInvalid and becameError on your model to handle these cases.
Hope this helps someone else; the docs certainly don't reflect this right now.
DS.RESTAdapter just got a bit more error handling in this commit but we are still not yet at a point where we have a great recommendation for error handling.
If you are ambitious/crazy enough to put apps in production today with ember-data (as I have been!), it is best to make sure that the likelihood of failures in your API is extremely low. i.e. validate your data client-side.
Hopefully, we can update this question with a much better answer in the coming months.
I just ran into such a situation, not sure if this is already explained anywhere.
I am using:
Em.VERSION : 1.0.0
DS.VERSION : "1.0.0-beta.6"
Ember Validations (dockyard) : Version: 1.0.0.beta.1
Ember I18n
The model was initially mixedin with Validation mixin.
App.Order = DS.Model.extend(Ember.Validations.Mixin, {
.....
someAttribute : DS.attr('string'),
/* Client side input validation with ember-validations */
validations : {
someAttribute : {
presence : {
message : Ember.I18n.t('translations.someAttributeInputError')
}
}
}
});
In the template, corresponding handlebars is added. (note that ember validations will automatically add errors to model.errors.<attribute> in case of input validations, I will be using same trade-off in server validations as well)
<p>{{t 'translations.myString'}}<br>
{{view Ember.TextField valueBinding="attributeName"}}
{{#if model.errors.attributeName.length}}<small class="error">{{model.errors.attributeName}}</small>{{/if}}
</p
Now, we will be saving the Order
App.get('order').save().then(function () {
//move to next state?
}, function(xhr){
var errors = xhr.responseJSON.errors;
for(var error in errors){ //this loop is for I18n
errors[error] = Ember.I18n.t(errors[error]);
}
controller.get('model').set('errors', errors); //this will overwrite current errors if any
});
Now if there is some validation error thrown from server, the returned packet being used is
{"errors":{"attributeName1":"translations.attributeNameEror",
"another":"translations.anotherError"}}
status : 422
It is important to use status 422
So this way, your attribute(s) can be validated client side and again on server side.
Disclaimer : I am not sure if this is the best way!
Since there's currently no good solution in stock Ember-Data, I made my own solution by adding an apiErrors property to DS.Model and then in my RestAdapter subclass (I already needed my own) I added error callbacks to the Ajax calls for createRecord and updateRecord that save the errors and put the model in the "invalid" state, which is supposed to mean client-side or server-side validations failed.
Here's the code snippets:
This can go in application.js or some other top-level file:
DS.Model.reopen({
// Added for better error handling on create/update
apiErrors: null
});
This goes in the error callbacks for createRecord and updateRecord in a RestAdapter subclass:
error: function(xhr, textStatus, err) {
console.log(xhr.responseText);
errors = null;
try {
errors = JSON.parse(xhr.responseText).errors;
} catch(e){} //ignore parse error
if(errors) {
record.set('apiErrors',errors);
}
record.send('becameInvalid');
}