I'm trying to create an ember data record with a hasMany relationship from a POJO, and it's been remarkably difficult. This is using ember-concurrency (hence the task and yield syntax.
saveEntry: task(function* (obj){
let entry = yield this.model.get('entries').createRecord({
groupId: obj.objectID,
name: obj.name,
...
}).save();
obj.owner_ids.forEach((user_id) => {
this.store.findRecord('user', user_id).then((user) => {
entry.get('owners').pushObject(user);
entry.save();
});
})
}).drop();
So the tough bits are the 'obj.owner_ids', which is an array of user_ids. I add each to the array, but then need to save after every pushObject or it doesn't work. And even when it does work, it is throwing a bunch of network errors (probably due to race conditions on the save). There must be a better way, but I haven't found it.
I'm not sure what this.model represents in your code, but you could try fetching all of the users you need and then attaching them at the time you create and save like:
import { all } from 'rsvp';
saveEntry: task(function* (obj){
let users = obj.owner_ids.map(userId => this.store.findRecord('user', user_id));
let promises = this.model.get('entries').createRecord({
groupId: obj.objectID,
name: obj.name,
owners: users,
...
}).save()
// all() waits for an an array of promises to return
yield all(promises);
}).drop();
Related
I am processing a bunch of records and producing an array of hashes for a third party library. For the life of me I can't figure out why this doesn't work.
export default Route.extend({
model: function(params) {
let qp = {viewName: 'byDay'};
return this.store.query('job-receipt', qp).then(
(receipts)=>
all(
receipts.map(
(receipt)=>
hash({
stockCode: receipt.get('job')
.then(job => job.get('stockCode'))
.then(stockCode => stockCode.get('stockCode')),
productClass: receipt.get('job')
.then(job => job.get('stockCode'))
.then(stockCode => stockCode.get('productClass'))
.then(productClass => productClass.get('descr')),
qtyRecvd: receipt.get('qtyRecvd')
})
)
)
);
If I keep reentering the route, eventually the promises resolve. If I check, the productClass promise just straight up gets called with a null value. Why isn't it waiting for the stockCode.get('productClass') to resolve? I know there are real values in there because it does eventually resolve.
I'm missing something super basic. I've tried Ember.get(thing, keypath) etc. Don't these all return promises? Isn't the RSVP.hash supposed to wait for all the promises to resolve before proceeding? Like I said, I know the data is good because eventually it does resolve (as opposed to me just not handling a rejection).
EDIT:
I changed the productClass promise to this:
productClass: receipt.get('job')
.then(job => job.get('stockCode'))
.then(stockCode => stockCode.get('productClass'))
.then(productClass => {if (!productClass) {return 'foo';} return productClass.get('descr');})
Now the report renders correctly every time albeit with nonsense. If I navigate to another route, and then back to this route, it renders perfectly. So, that makes it hard for me to believe I have some kind of data error. And even some of the stock codes return the right product class - not 'foo' - on the first run through. I'm not even sure how to debug this further.
edit
Just saw this. May be a bug after all.
[3.2.0+] Snapshot’s related data has become null #5565
It turns out it is a bug. The bug for belongs-to.js causes the model to not wait for internalModel to complete loading before resolving the promise. The fix linked below resolves the issue
[BUGFIX] use internalModel promise if already loading #5562
I think the main issue is that "reciepts.job" is likely a DS.belongsTo relationship right? If you switch it to load job: DS.belongsTo('job', {async: false}), that will force ember-data to load that property synchronously (and will save a lot of headaches). But that requires the data to be available either in the json response.
If that doesn't work, you should investigate ember concurrency. Use that to clean up your code to look a little more straightforward. You'll have to fill in some blanks or change things where I misunderstood your use case, but this is a likely a good starting point.
The idea is to continue to break all the asynchronous calls into seperate tasks. Each ember-concurrency task object returns a promise, so you can keep bundling them up until you get to model which you can return just like any other promise.
//model.js
import {task} from 'ember-concurrency';
.....
model() {
return this.get('loadData').perform();
},
loadData: task(function*(){
let reciepts = yield this.store.query('job-receipt', qp);
let promises = reciepts.map(r => {
return this.get('loadNestedData').perform(r);
})
return all(promises)
}),
loadNestedData: task(function*(reciept) {
let job = yield receipt.get('job');
return hash({
stockCode: job.get('sockcode')
});
})
The question is about the interaction of a mutation, optimistic response, and a watchQuery.
I have a mutation "myMutation" which has an "optimisticResponse" and an implemented "update" function.
Every time I do a mutation query the "update" function is called twice, the first time with optimistic response data and the second one with real data. All is Ok and all as described in the documentation.
Into my "update" function I modify "myQuery" cache data through using readQuery/writeQuery methods.
Every time I modify "myQuery" cache data a watchQuery (based on "myQuery") subscription is called. All is Ok and all as described in the documentation.
But the problem is that I cannot distinguish into my watchQuery whether I receive optimistic response data or real response data. It is crucial for me because the reaction must be different since valuable part of data can be provided by a server only.
I should show a GUI element with a special style when I receive an optimistic response and I should prohibit any interactions with it until I receive a real response.
Unfortunately, I can't solve this matter. At a glance, there is no difference between optimistic and real responses. I've googled a lot and haven't found a solution. The only idea I have is adding a special field to my GraphQL data which will show whether a response is received from a server or not. But it looks ugly and smells bad. I am sure, there must be a simple correct way to overcome the problem.
Maybe there is an easier way or there will be one in the future but here is what I know.
The data in optimisticResponse is only provided during the first call to update. That is where you can flag to your update function that it is dealing with optimistic data. You can put any data you want there. I put isOptimistic: true,.
To deal with the watchQuery issue, I recommend you make use of apollo-link-state to add a client-only field or fields to the areas of your data model where optimistic upserts should be known to the display. Don't include isOptimistic in your mutation query so you know it's from the server and not the optimistic response and force it to false if it's not true. See this example:
const SUBMIT_COMMENT_MUTATION = gql`
mutation submitComment($repoFullName: String!, $commentContent: String!) {
submitComment(repoFullName: $repoFullName, commentContent: $commentContent) {
postedBy {
login
html_url
}
createdAt
content
}
}
`;
const CommentsPageWithMutations = ({ currentUser }) => (
<Mutation mutation={SUBMIT_COMMENT_MUTATION}>
{(mutate) => (
<CommentsPage
submit={(repoFullName, commentContent) =>
mutate({
variables: { repoFullName, commentContent },
optimisticResponse: {
__typename: 'Mutation',
submitComment: {
__typename: 'Comment',
postedBy: currentUser,
createdAt: new Date(),
content: commentContent,
isOptimistic: true, // Only provided to update on the optimistic call
},
},
update: (proxy, { data: { submitComment } }) => {
// Make sure CommentAppQuery includes isOptimistic for each comment added by apollo-link-state
// submitComment.isOptimistic will be undefined here if it's from the server
const newComment = {
...submitComment,
isOptimistic: submitCommit.isOptimistic ? true : false,
};
// Read the data from our cache for this query.
const data = proxy.readQuery({ query: CommentAppQuery });
// Add our comment from the mutation to the end.
data.comments.push(newComment);
// Write our data back to the cache.
proxy.writeQuery({ query: CommentAppQuery, data });
},
})
}
/>
)}
</Mutation>
);
See https://www.apollographql.com/docs/link/links/state.html.
I couldn't get this to work on Apollo 3.X by only adding a property on the optimistic response, the property was getting stripped away. To get it to work I had to add a local variable to the query.
fragment object on Object {
id
isOptimistic #client
...
Once that is done, I was able to add the local-only flag to my optimistic response.
const optimisticResponse = {
object: {
id: "temp-id",
isOptimistic: true,
...
}
}
I have two screens:
Screen1: Results
Screen2: Edit Filters
When I edit the filters on Screen2 and press back, I would like to refetch the query on Screen1 (with the newly built filter string variable). Editing the filters doesn't use a mutation or fire any Redux actions (I'm storing the users search filters/preferences in localStorage/AsyncStorage instead of a database, so no mutation). I'm merely changing the local state of the form and use that to build a filter string that I want to pass to a certain query on Screen1. I have access to the filter string on both screens if that helps.
It seems like refetch() is limited to the component its query wraps http://dev.apollodata.com/react/receiving-updates.html#Refetch so how would I re-run the query from a different screen?
I tried putting the same query on both Screen1 and Screen2, then calling the refetch on Screen2, and although the query works and gets the new data on Screen2, the same name query doesn't update on Screen1 where I actually need it. Isn't it supposed to if they have the same name? (but the filters variable changed)
If I am just designing this incorrectly and there is an easier way to do it, please let me know. I expect that if I have 2 screens, put the same query on both of them, and refetch one of the queries with a new filters variable, then the refetch should happen in both places, but it's currently treating them individually.
I did the same thing here. The scenario:
- I choose a peer to filter some messages.
- I keep the peerId into redux
- I make both components (the filter and the list) dependent on that redux value.
Like this:
1 - To put that filter value on redux (and to grab it back):
import { compose, graphql } from 'react-apollo'
import { connect } from 'react-redux';
...
export default compose(
connect(
(state,ownProps) => ({
selectedMessages: state.messages.selectedMessages,
peerId: state.messages.peerId
}),
(dispatch) => ({
clearSelection: () => dispatch(clearSelection()),
setPeer: (peerId) => dispatch(setPeer(peerId))
})
),
graphql(
PEERS_QUERY,
...
when you call connect first (using compose), before you call a graphql wrapper, or outside that wrapper, you will have peerId available as a prop on your graphql wrapper, so you can use it to filter your query:
export default compose(
connect(
(state,ownProps) => {
return {
peerId: state.messages.peerId,
selectedMessages: state.messages.selectedMessages
}
},
(dispatch) => ({
toggleMessage(messageId) {
dispatch(toggleMessage(messageId));
}
})
),
graphql( // peerId is available here because this is wrapped by connect
MESSAGES_QUERY,
{
options: ({peerId}) => ({variables:{peerId:peerId}}),
skip: (ownProps) => ownProps.peerId === '',
props: ({
...
...
...
)(MessageList);
UPDATE
Note that this question applies to Ember Data pre-1.0 beta, the mechanism for loading relationships via URL has changed significantly post-1.0 beta!
I asked a much longer question a while back, but since the library has changed since then, I'll ask a much simpler version:
How do you use DS.Adapter.findHasMany? I am building an adapter and I want to be able to load the contents of a relationship on get of the relationship property, and this looks like the way to do it. However, looking at the Ember Data code, I don't see how this function can ever be called (I can explain in comments if needed).
There's not an easy way with my backend to include an array of ids in the property key in the JSON I send--the serializer I'm using doesn't allow me to hook in anywhere good to change that, and it would also be computationally expensive.
Once upon a time, the Ember Data front page showed an example of doing this "lazy loading"...Is this possible, or is this "Handle partially-loaded records" as listed on the Roadmap, and can't yet be done.?
I'm on API revision 11, master branch as of Jan 15.
Update
Okay, the following mostly works. First, I made the following findHasMany method in my adapter, based on the test case's implementation:
findHasMany: function(store, record, relationship, details) {
var type = relationship.type;
var root = this.rootForType(type);
var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);
this.ajax(url, "GET", {
success: function(json) {
var serializer = this.get('serializer');
var pluralRoot = serializer.pluralize(root);
var hashes = json[pluralRoot]; //FIXME: Should call some serializer method to get this?
store.loadMany(type, hashes);
// add ids to record...
var ids = [];
var len = hashes.length;
for(var i = 0; i < len; i++){
ids.push(serializer.extractId(type, hashes[i]));
}
store.loadHasMany(record, relationship.key, ids);
}
});
}
Prerequisite for above is you have to have a well-working extractId method in your serializer, but the built-in one from RESTAdapter will probably do in most cases.
This works, but has one significant problem that I haven't yet really gotten around in any attempt at this lazy-loading approach: if the original record is reloaded from the server, everything goes to pot. The simplest use case that shows this is if you load a single record, then retrieve the hasMany, then later load all the parent records. For example:
var p = App.Post.find(1);
var comments = p.get('comments');
// ...later...
App.Post.find();
In the case of only the code above, what happens is that when Ember Data re-materializes the record it recognizes that there was already a value on the record (posts/1), tries to re-populate it, and follows a different code path which treats the URL string in the JSON hash as an array of single-character IDs. Specifically, it passes the value from the JSON to Ember.EnumerableUtils.map, which understandably enumerates the string's characters as array members.
Therefore, I tried to work around this by "patching" DS.Model.hasManyDidChange, where this occurs, like so:
// Need this function for transplanted hasManyDidChange function...
var map = Ember.EnumerableUtils.map;
DS.Model.reopen({
});
(^ Never mind, this was a really bad idea.)
Update 2
I found I had to do (at least) one more thing to solve the problem mentioned above, when a parent model is re-loaded from the server. The code path where the URL was getting split into single-characters was in DS.Model.reloadHasManys. So, I overrode this method with the following code:
DS.Model.reopen({
reloadHasManys: function() {
var relationships = get(this.constructor, 'relationshipsByName');
this.updateRecordArraysLater();
relationships.forEach(function(name, relationship) {
if (relationship.kind === 'hasMany') {
// BEGIN FIX FOR OPAQUE HASMANY DATA
var cachedValue = this.cacheFor(relationship.key);
var idsOrReferencesOrOpaque = this._data.hasMany[relationship.key] || [];
if(cachedValue && !Ember.isArray(idsOrReferencesOrOpaque)){
var adapter = this.store.adapterForType(relationship.type);
var reloadBehavior = relationship.options.reloadBehavior;
relationship.name = relationship.name || relationship.key; // workaround bug in DS.Model.clearHasMany()?
if (adapter && adapter.findHasMany) {
switch (reloadBehavior) {
case 'ignore':
//FIXME: Should probably replace this._data with references/ids, currently has a string!
break;
case 'force':
case 'reset':
default:
this.clearHasMany(relationship);
cachedValue.set('isLoaded', false);
if (reloadBehavior == 'force' || Ember.meta(this).watching[relationship.key]) {
// reload the data now...
adapter.findHasMany(this.store, this, relationship, idsOrReferencesOrOpaque);
} else {
// force getter code to rerun next time the property is accessed...
delete Ember.meta(this).cache[relationship.key];
}
break;
}
} else if (idsOrReferencesOrOpaque !== undefined) {
Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter);
Ember.assert("You tried to load many records but your adapter does not implement `findHasMany`", adapter.findHasMany);
}
} else {
this.hasManyDidChange(relationship.key);
}
//- this.hasManyDidChange(relationship.key);
// END FIX FOR OPAQUE HASMANY DATA
}
}, this);
}
});
With that addition, using URL-based hasManys is almost usable, with two main remaining problems:
First, inverse belongsTo relationships don't work correctly--you'll have to remove them all. This appears to be a problem with the way RecordArrays are done using ArrayProxies, but it's complicated. When the parent record gets reloaded, both relationships get processed for "removal", so while a loop is iterating over the array, the belongsTo disassociation code removes items from the array at the same time and then the loop freaks out because it tries to access an index that is no longer there. I haven't figured this one out yet, and it's tough.
Second, it's often inefficient--I end up reloading the hasMany from the server too often...but at least maybe I can work around this by sending a few cache headers on the server side.
Anyone trying to use the solutions in this question, I suggest you add the code above to your app, it may get you somewhere finally. But this really needs to get fixed in Ember Data for it to work right, I think.
I'm hoping this gets better supported eventually. On the one hand, the JSONAPI direction they're going explicitly says that this kind of thing is part of the spec. But on the other hand, Ember Data 0.13 (or rev 12?) changed the default serialized format so that if you want to do this, your URL has to be in a JSON property called *_ids... e.g. child_object_ids ... when it's not even IDs you're sending in this case! This seems to suggest that not using an array of IDs is not high on their list of use-cases. Any Ember Data devs reading this: PLEASE SUPPORT THIS FEATURE!
Welcome further thoughts on this!
Instead of an array of ids, the payload needs to contain "something else" than an array.
In the case of the RESTAdapter, the returned JSON is like that:
{blog: {id: 1, comments: [1, 2, 3]}
If you want to handle manually/differently the association, you can return a JSON like that instead:
{blog: {id: 1, comments: "/posts/1/comments"}
It's up to your adapter then to fetch the data from the specified URL.
See the associated test: https://github.com/emberjs/data/blob/master/packages/ember-data/tests/integration/has_many_test.js#L112
I was glad to find this post, helped me. Here is my version, based off the current ember-data and your code.
findHasMany: function(store, record, relationship, details) {
var adapter = this;
var serializer = this.get('serializer');
var type = relationship.type;
var root = this.rootForType(type);
var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);
return this.ajax(url, "GET", {}).then(function(json) {
adapter.didFindMany(store, type, json);
var list = $.map(json[relationship.key], function(o){ return serializer.extractId(type, o);});
store.loadHasMany(record, relationship.key, list);
}).then(null, $.rejectionHandler);
},
for the reload issue, I did this, based on code I found in another spot, inside the serializer I overrode:
materializeHasMany: function(name, record, hash, relationship) {
var type = record.constructor,
key = this._keyForHasMany(type, relationship.key),
cache = record.cacheFor('data');
if(cache) {
var hasMany = cache.hasMany[relationship.key];
if (typeof(hasMany) == 'object' || hasMany instanceof Object) {
record.materializeHasMany(name, hasMany);
return;
}
}
var value = this.extractHasMany(type, hash, key);
record.materializeHasMany(name, value);
}
I'm still working on figuring out paging, since some of the collections I'm working with need it.
I got a small step closer to getting it working with revision 13 and based myself on sfossen's findHasMany implementation. For an Ember model 'Author' with a hasMany relationship 'blogPosts', my rest api looks like '/api/authors/:author_id/blog_posts'. When querying the rest api for an author with id 11 the blog_posts field reads '/authors/11/blog_posts'.
I now see the related blog posts being returned by the server, but Ember still throws an obscure error that it can not read 'id' from an undefined model object when rendering the page. So I'm not quite there yet, but at least the related data is correctly requested from the rest service.
My complete adapter:
App.Adapter = DS.RESTAdapter.extend({
url: 'http://localhost:3000',
namespace: 'api',
serializer: DS.RESTSerializer.extend({
keyForHasMany: function(type, name) {
return Ember.String.underscore(name);
},
extractHasMany: function(record, json, relationship) {
var relationShip = relationship + '_path';
return { url : json[relationShip] }
}
}),
findHasMany: function(store, record, relationship, details) {
var type = relationship.type;
var root = this.rootForType(type);
var url = this.url + '/' + this.namespace + details.url;
var serializer = this.get('serializer');
return this.ajax(url, "GET", {}).then(
function(json) {
var relationship_key = Ember.String.underscore(relationship.key);
store.loadMany(type, json[relationship_key]);
var list = $.map(json[relationship_key], function(o){
return serializer.extractId(type, o);}
);
store.loadHasMany(record, relationship.key, list);
}).then(null, $.rejectionHandler);
}
});
Here is my solution but it is on Ember-data 0.14, so the world has moved on, even if we are still on this code base:
findHasMany: function(store, record, relationship, details) {
if(relationship.key !== 'activities') {
return;
}
var type = relationship.type,
root = this.rootForType(type),
url = this.url + details.url,
self = this;
this.ajax(url, "GET", {
data: {page: 1}
}).then(function(json) {
var data = record.get('data'),
ids = [],
references = json[relationship.key];
ids = references.map(function(ref){
return ref.id;
});
data[relationship.key] = ids;
record.set('data', data);
self.didFindMany(store, type, json);
record.suspendRelationshipObservers(function() {
record.hasManyDidChange(relationship.key);
});
}).then(null, DS.rejectionHandler);
},
I found replacing the data with the ids worked for me.
I have a model built from a JSON object.
// extend the json model to get all props
App.Model = Ember.Object.extend(window.jsonModel);
I want to automatically save the model when anything is updated. Is there any way I can add an observer to the whole model?
EDIT: // adding the solution I currently go
For now I do:
// XXX Can't be right
for (var prop in window.jsonModel) {
if (window.jsonModel.hasOwnProperty(prop)) {
App.model.addObserver(prop, scheduleSave);
}
}
This is a large form, which means I'm adding tons of observers – it seems so inefficient.
A firebug breakpoint at Ember.sendEvent() reveals that there are events called App.model.lastName:change being sent. I could hack in an intercept there, but was hoping for an official way.
You can bind to isDirty property of subclass of DS.Model. The isDirty changes from false to true when one of model properties changes. It will not serve well for all cases because it changes only once until reset or committed, but for your case -
I want to automatically save the model when anything is updated. Is there any way I can add an observer to the whole model?
it may work fine.
From the article:
autosave: function(){
this.save();
}.observes('attributes'),
save: function(){
var self = this,
url = this.get('isNew') ? '/todos.json' : '/todos/'+this.get('id')+'.json',
method = this.get('isNew') ? 'POST' : 'PUT';
$.ajax(url, {
type: 'POST',
// _method is used by Rails to spoof HTTP methods not supported by all browsers
data: { todo: this.get('attributes'), _method: method },
// Sometimes Rails returns an empty string that blows up as JSON
dataType: 'text',
success: function(data, response) {
data = $.trim(data);
if (data) { data = JSON.parse(data); }
if (self.get('isNew')) { self.set('id', data['todo']['id']); }
}
});
},
isNew: function(){
return !this.get('id');
}.property('id').cacheable(),
I had the same requirement, and not finding a suitable answer, I implemented one.
Try this: https://gist.github.com/4279559
Essentially, the object you want to observe all the properties of MUST be a mixed of Ember.Stalkable. You can observe the properties of that object as 'item.#properties' (or, if you bake observers directly on the Stalkable, '#properties' alone works. "#ownProperties", "#initProperties" and "#prototypeProperties" also work, and refer to (properties that are unique to an instance and not defined on any prototype), (properties that are defined as part of the create() invocation), and (properties that are defined as part of the class definition).
In your observers, if you want to know what properties changed and invoked the handler, the property "modifiedProperties", an array, will be available with the names of the changed properties.
I created a virtual property _anyProperty that can be used as a dependent key:
import Ember from 'ember';
Ember.Object.reopen({
// Virtual property for dependencies on any property changing
_anyPropertyName: '_anyProperty',
_anyProperty: null,
propertyWillChange(keyName) {
if (keyName !== this._anyPropertyName) {
this._super(this._anyPropertyName);
}
return this._super(keyName);
},
propertyDidChange(keyName) {
if (keyName !== this._anyPropertyName) {
this._super(this._anyPropertyName);
}
return this._super(keyName);
}
});