I have a many-to-many relationship defined between my tag and payment models as shown below.
//models/tag.js
import Model, { attr, hasMany } from '#ember-data/model';
export default Model.extend({
name: attr('string'),
backgroundColour: attr('string'),
textColour: attr('string'),
payments: hasMany('payment')
});
// models/payment.js
import Model, { attr, hasMany } from '#ember-data/model';
export default Model.extend({
date: attr('date'),
amount: attr('number'),
paymentId: attr('string'),
tags: hasMany('tag'),
});
By default, when I add tags to payments, the id of the payment is used as the key for the relationship. My aim is for Ember data to use the paymentId property as the key for this relationship instead.
The snippet below shows the structure of the data I'm loading, where a tag references payments by the paymentId property.
// Example tag
{
"id": "25",
"name": "Groceries",
"backgroundColour": "31b04b",
"textColour": "ffffff",
"payments": ["20190121201902210"] // References paymentId rather than id
},
// Example payment
{
"id": "1"
"date": "2019-01-27T22:00:00.000Z",
"amount": 1644.44,
"paymentId": "20190121201902210",
"tags": ["25"]
}
I've tried to customise the payment serializer as below,
// serializers/payment.js
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
keyForRelationship(key, _relationship) {
if (key === 'payments') {
return 'paymentId';
}
},
});
However, when the models are loaded I get this error: Assertion Failed: All elements of a hasMany relationship must be instances of Model, you passed [ "20190121201902210" ].
How can I make Ember data use paymentId rather than id when looking up related payments?
I assume that you are using RESTSerializer. It has a primaryKey option, which should be used if the primary key is not named id in your API payload.
It seems to be a little bit difficult for your example as that record seems to have two primary keys: id and paymentId. If only one of them is used to reference related records I would recommend to simply ignore the other one.
If both are used to reference related records, you are in a bad situation. Maybe you can change the API?
If that's not possible I guess you need to map one ID to the other in a serializer which requires to have the payment record loaded before. That gets tricky as serializers are sync which means the record must be loaded before. I guess you will face a lot of edge cases until such a solution is stable - and breaking it would be quiet easy as it highly depends on timing.
So maybe you should even consider not using Ember Data for these resources at all. It highly depends on the assumption that each resource could be identified by a combination of it's type and ID. But it sounds like for you the same resource could be identified by two different IDs.
Related
I'm still learning ember.js and have run into a roadblock with ember data not resolving lookup relationships in models. I have one model 'site' that will be basically a lookup table for every other model to differentiate data based on location.
At this point, I'm doing something wrong or missing a key concept - probably both... (or maybe it's the wee hour!)
Site Model (i.e. the lookup table)
import DS from 'ember-data';
export default DS.Model.extend({
code: DS.attr(),
name: DS.attr(),
});
The site model would have a hasMany relationship to all my other models (will be about 12 when complete)
Associate Model
import DS from 'ember-data';
import { belongsTo } from 'ember-data/relationships';
export default DS.Model.extend({
site: belongsTo('site'),
last: DS.attr(),
first: DS.attr(),
active: DS.attr('boolean'),
fullName: Ember.computed('first', 'last', function() {
return `${this.get('first')} ${this.get('last')}`;
}),
});
The 'associate model' will also be a lookup along with 'site' in some other models.
I'm providing data via the JSON API spec but I'm not including the relationship data because as I understand it, ember data it should be pulling down the site data using the site id attribute.
{
"links": {
"self": "/maint/associates"
},
"data": [
{
"type": "associate",
"id": "1",
"attributes": {
"site": "6",
"last": "Yoder",
"first": "Steven",
"active": "1"
},
"links": {
"self": "/associates/1"
}
}
]
}
In my template file I'm referencing associate.site which gives me an error.
<(unknown mixin):ember431>
If I use associate.code or .name to match the site model, nothing will show in the template. The code from the 'site' table is the data I really want to displayed in the template.
So the obvious questions:
Am I wrong that Ember Data should be resolving this or do I need to
include the relationship in my API response?
I realize that my belongsTo in the 'associate' model only references
site while I want site.code, so how do I make that relationship
known or access the field in my 'associate' model?
I didn't include hasMany relationship in the 'site' model because
there would be many. Do I need to do an inverse relationship in
other models? Examples I've seen don't all show the hasMany
relationships setup.
When I look at the models in ember inspector the site field is not
included in the model. Even if I wasn't getting the correct data
should it still show up?
I like ember so far, just need to understand and get over this roadblock
Update: My backend JSON library would only generate relationship links based on the current spec which would be
"related": "/streams/1/site"
but ember data does call
"related": "/sites/1"
to resolve the relationship
So #Adam Cooper answer is correct if you generate links as he answered or if you can only generate the links based on the current specification.
If you're using the JSONAPIAdapter, which is the default, you want your response to look this:
{
"links": {
"self": "/maint/associates"
},
"data": [{
"type": "associate",
"id": "1",
"attributes": {
"last": "Yoder",
"first": "Steven",
"active": "1"
},
relationships: {
"site": {
"links": {
related: "/sites/6"
}
}
}
}]
}
That will allow Ember Data to look up the site via its relationship. Right now Ember is trying to access the site model which Ember Data can't populate hence the error you're getting. As an aside you could probably do with returning an actual boolean value for active too.
This is a similar question to this one, except this is for the latest versions of Ember and Active Model Serializers (0.10.2).
I have a simple Parent:Child relationship.
app/models/trail.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr(),
// relationships
employees: DS.hasMany('employee', { async: true }),
});
app/models/employee.js
import DS from 'ember-data';
import Person from '../models/person';
export default Person.extend({
status: DS.attr(),
statusCode: DS.attr(),
});
app/models/person.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
avatarUrl: DS.attr(),
firstName: DS.attr(),
lastName: DS.attr(),
fullName: Ember.computed('firstName', 'lastName', function() {
return `${this.get('lastName')}, ${this.get('firstName')}`;
}),
});
When I create a new Trail, and select two employees for the 'hasMany', the following json arrives the server (from the Rails log):
{"data":
{"attributes":
{"name":"TEST3",
"gpx-file-url":"a url",
"distance-value":"5"},
"relationships":
{"employees":{"data":[]}}, "type":"trails"}}
My question is, what has happened to the employees? Where are the id's of the employees (they already exist both in the database and in the Ember Store - ie, I am not trying to create child records in this request).
EDIT
I just found this question, which explains that the id's for a hasMany relationship are not sent by Ember's JSONAPISerializer to the API - since the foreign key here actually has to be persisted in each child record. So essentially by 'selecting' employees, you need to save the fact that they now have a parent. So the selected employee records need to be persisted.
But my understanding was that this all works "out of the box" and that Ember would automatically fire a POST request to do this, but that seems to not be the case.
This then gets to the real question - how do I update those children?
UPDATE - BOUNTY ADDED AS THIS HAS QUESTION HAS EVOLVED
After further analysis, it became clear that a new model was required - Assignments. So now the problem is more complex.
Model structure is now this:
Trail
hasMany assignments
Employee
hasMany assignments
Assignment
belongsTo Trail
belongsTo Employee
In my 'new Trail' route, I use the fantastic ember-power-select to let the user select employees. On clicking 'save' I plan to iterate through the selected employees and then create the assignment records (and obviously save them, either before or after saving the Trail itself, not sure which is best yet).
The problem is still, however, that I don't know how to do that - how to get at the 'selected' employees and then iterate through them to create the assignments.
So, here is the relevant EPS usage in my template:
in /app/templates/trails/new.hbs
{{#power-select-multiple options=model.currentEmployees
searchPlaceholder="Type a name to search"
searchField="fullName"
selected=staff placeholder="Select team member(s)"
onchange=(route-action 'staffSelected') as |employee|
}}
<block here template to display various employee data, not just 'fullName'/>
{{/power-select-multiple}}
(route-action is a helper from Dockyard that just automatically sends the action to my route, works great)
Here is my model:
model: function () {
let myFilter = {};
myFilter.data = { filter: {status: [2,3] } }; // current employees
return Ember.RSVP.hash({
trail: this.store.createRecord('trail'),
currentEmployees: this.store.query('employee', myFilter).then(function(data) {return data}),
});
},
actions: {
staffSelected (employee) {
this.controller.get('staff').pushObject(employee);
console.log(this.controller.get('staff').length);
},
}
I only discovered today that we still need controllers, so this could be my problem! Here it is:
import Ember from 'ember';
export default Ember.Controller.extend({
staff: [] <- I guess this needs to be something more complicated
});
This works and I see one object is added to the array in the console. But then the EPS refuses to work because I get this error in the console:
trekclient.js:91 Uncaught TypeError: Cannot read property 'toString' of undefined(anonymous function) # trekclient.js:91ComputedPropertyPrototype.get # vendor.js:29285get #
etc....
Which is immediately follow by this:
vendor.js:16695 DEPRECATION: You modified (-join-classes (-normalize-class "concatenatedTriggerClasses" concatenatedTriggerClasses) "ember-view" "ember-basic-dropdown-trigger" (-normalize-class "inPlaceClass" inPlaceClass activeClass=undefined inactiveClass=undefined) (-normalize-class "hPositionClass" hPositionClass activeClass=undefined inactiveClass=undefined) (-normalize-class "vPositionClass" vPositionClass activeClass=undefined inactiveClass=undefined)) twice in a single render. This was unreliable in Ember 1.x and will be removed in Ember 3.0 [deprecation id: ember-views.render-double-modify]
So I imagine this is because the examples in the documentation just uses an array containing strings, not actual Ember.Objects. But I have no clue how to solve this.
So, I decided to throw away the controller (ha ha) and get creative.
What if I added a property to the Trail model? This property can basically be a 'dummy' property that collected the selected employees.
in /app/models/trail.js
selectedEmps: DS.hasMany('employee', async {false})
I set async to false since we will not persist them and before saving the new Trail I can just set this to null again.
in /app/templates/trails/new.js
{{#power-select-multiple options=model.currentEmployees
searchPlaceholder="Type a name to search"
searchField="fullName"
selected=model.selectedEmps placeholder="Select team member(s)"
onchange=(action (mut model.selectedEmps)) as |employee|
}}
<block here again/>
{{/power-select-multiple}}
This works, it doesn't 'blow up' after selecting the first employee. I can select multiple and delete them from the template. The control seems to work fine, as it is mutating 'model.selectedEmps' directly.
Now, I think this is a hack because I have two problems with it:
If I change the 'mut' to an action, so I can add further logic, I
cannot figure out how to access what is actually stored in the
propery 'model.selectedEmps'
Even if I can figure out (1) I will have to always make sure that
'selectedEmps' is emptied when leaving this route, otherwise the
next time this route is entered, it will remember what was
selected before (since they are now in the Ember.Store)
The fundamental issue is that I can live with 'mut' but still have the problem that when the user hits 'Save' I have to figure out which employees were selected, so I can create the assignments for them.
But I cannot figure out how to access what is selected. Maybe something this Spaghetti-Monster-awful mess:
save: function (newObj) {
console.log(newObj.get('selectedEmps'));
if (newObj.get('isValid')) {
let emp = this.get('store').createRecord('assignment', {
trail: newObj,
person: newObj.get('selectedEmps')[0]
})
newObj.save().then( function (newTrail) {
emp.save();
//newTrail.get('selectedEmps')
// this.transitionTo('trails');
console.log('DONE');
});
}
else {
alert("Not valid - please provide a name and a GPX file.");
}
},
So there are two problems to solve:
How to get the selected employees, iterate and create the
assignments.
How to then save the results to the API (JSON-API using Rails). I
presume that newObj.save and each assignment.save will take care
of that.
UPDATE
The developer of EPS kindly pointed out that the action handler receives an array, since I changed to using a multiple select, not a single select as it had been earlier. So the action is receiving the full array of what is currently selected. DOH!
I was thus able to update the action handler as follows, which now successfully stores the currently selected employees in the staff property of the controller. One step closer.
staffSelected(newList) {
existing.forEach(function(me){
if (!newList.includes(me)) {
existing.removeObject(me); // if I exist but the newList doesn't have me, remove me
}
});
newList.forEach(function(me){
if (!existing.includes(me)) {
existing.pushObject(me); // if I don't exist but the newList has me, add me
}
});
}
Perhaps not the best way to intersect 2 arrays but that's the least of my concerns at 4am on a Saturday night. :(
FINAL PROBLEM UPDATE - how to save the data?
Ok, so now that I can get the selected employees, I can create assignments, but still cannot figure out what Ember requires for me to save them, this save action throws an error:
save: function (newObject) {
if (newObject.get('isValid')) {
let theChosenOnes = this.controller.get('theChosenOnes');
let _store = this.get('store');
theChosenOnes.forEach(function (aChosenOne) {
_store.createRecord('assignment', {
trail: newObject,
person: aChosenOne,
});
});
newObject.save().then(function (newTrail) {
newTrail.get('assignments').save().then(function() {
console.log('DONE');
});
});
}
get(...).save is not a function
The problem with your final update is that in Ember Data 2.x, relationships are asynchronous by default, so what's returned from newTrail.get('assignments') is not a DS.ManyArray, which has a .save, but a PromiseArray, which doesn't have that.
You need a small tweak to do this instead, so you call .save on the resolved relationship:
newObject.save().then(function (newTrail) {
newTrail.get('assignments').then(assignments => assignments.save()).then(function() {
console.log('DONE');
});
});
Ember : 1.13.3
Ember Data : 1.13.5
jQuery : 1.11.3
I am trying to send a JSON payload using ember-data from my EmberJS client to my server. I want to send the entire object graph to the server on saving the project, as I don't want to send multiple requests. I wouldn't mind sending multiple requests, but I am worried about what happens if one of the requests fails in the middle and the data on the server will not be correct.
I wanted to use JSONAPI (http://jsonapi.org/format/#document-compound-documents) as that is becoming the default adapter in Ember. Also, there is a few C# libraries that handle this format, so I thought it would be quite straightforward. However, after reading the spec, it seems that I cannot embed objects if they do not have an id. EmberJS also does not attach the child objects to the JSON either even though I have specified { async: false, embedded: 'always' }) on the DS.attr.
My question is: If an application is used in such a way that an object graph is created on the client side, how do you use JSONAPI format to send the entire object graph to the server? Do I have to generate ids on the client side to satisfy the JSONAPI standard? Then once they get to the server just ignore them so they get saved with an id generated by the ORM?
Here is my labelGroup model:
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
labels: DS.hasMany('label-model', { async: false, embedded: 'always' })
});
Here is my project model:
import DS from 'ember-data';
export default DS.Model.extend(DS.EmbeddedRecordsMixin, {
name: DS.attr('string'),
labelGroups: DS.hasMany('labelGroup', { async: false, embedded: 'always'})
});
Here is the POST that I get after doing a save() on the project:
{
"data":{
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null}
]
}
},
"type":"label-projects"
}
}
UPDATE: I tried using https://www.npmjs.com/package/ember-cli-uuid to generate client side ids which it has. However the data getting output does not include the extra objects, only a reference to their ids. I expected to see an "included" property as specified here:http://jsonapi.org/format/#document-compound-documents, but it is not there.
{
"data":{
"id":"7b4544ee-91cd-493d-8b10-52040e68c283",
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":"08115273-e82a-4d46-93ea-232ce071fb78"},
{"type":"label-groups","id":"9ca94fe9-8077-411e-98d2-1694c6fecce4"},
{"type":"label-groups","id":"d629f1e8-7962-404d-8034-38229ab21f77"},
{"type":"label-groups","id":"c6bda655-5489-4760-847b-bf02239bb2c5"},
{"type":"label-groups","id":"f6fef249-2d1d-43f0-ba64-24b7ff8b5637"},
{"type":"label-groups","id":"a7db25bf-52c8-477b-83e4-64e7c76b072e"},
{"type":"label-groups","id":"f3b5fbb3-261a-4b3d-b481-b9352f8ce2d6"}
]
}
},
"type":"label-projects"
}
}
Ember-data has no support for what you want at the moment. So ember-data will not save your relationships data in a save payload.
But its possible to do this your own by using a custom adapter and serializer.
I strongly recommend you to checkout the API and then look into the source.
If you call .save() on your Model the createRecord method is called on your adapter.
Here serializeIntoHash on the serializer is called to serialize the Model.
serializeIntoHash calls serialize, where serializeBelongsTo and serializeHasMany is called.
Now you can just override serializeHasMany and modify the hasMany before the line:
json[payloadKey] = hasMany;
Here you have the type and the ids as they are sent by ember-data. You could just .forEach the data on the hasMany and then fetch the store for the data and build your included array.
I hope this helps you to understand the serializer and the adapter so you can modify it to do whatever you want pretty easy. Actually this is the best part about ember-data. The structure of the adapter and the serializer, which allows easy modifications.
I am using Ember Data with the RestAdapter and the following models:
Pizzas.Pizza = DS.Model.extend({
name: DS.attr('string'),
orders: DS.hasMany('order', { async: true }),
});
Pizzas.Order = DS.Model.extend({
date: DS.attr('date'),
pizzas: DS.hasMany('pizza', { async: true }),
});
I create and save a new order as follows:
var pizza1 = an existing pizza with id 1;
var pizza2 = an existing pizza with id 2;
var newOrder = this.store.createRecord('order');
newOrder.set('date', Date());
newOrder.get('pizzas').then(function(pizzas) {
pizzas.pushObject(pizza1);
pizzas.pushObject(pizza2);
newOrder.save();
});
This works well - Ember Data performs a POST on the Order model which include the ids of the pizzas in the pizzas relationship field. However, I expected that following the save, the order id would be automatically added to the orders relationship of the 2 pizza objects by Ember Data, but that appears not to be the case. This causes 2 issues:
When I ask for all the orders for a pizza, the new order does not appear (since it was never added to the relationship field of the pizza)
When a change is made to a pizza (e.g. name change) and the pizza saved, the new order relationship is lost on the server (since it was never added to the relationship field of the pizza and the PUT only includes the order ids last fetched from the server)
I solve this by amending the last line of code above as follows:
newOrder.save().then(function() {
pizza1.get('orders').then(function(orders) {
orders.pushObject(newOrder);
})
// same for pizza 2
} );
Does Ember data require that the relationship be created manually on both sides (as I am doing) or am I missing something?
I am using beta 11 plus patches from my own fork.
Persisting relationships is something you will need to manage yourself. There can't be any hard or fast rules in Ember Data about this because different servers and json apis will have different approaches to managing relationships as well as specific validation and referential integrity rules which will dictate how relationships between models should be managed. This will introduce ordering dependencies into your model persistence stategies. If you have a lax "NoSQL" type document server that has no such referential integrity requirements then things will appear easy at first, with the eventual reality of data inconsistencies, orphans, dangling references and so on.
Be careful about client side data persistence layers that claim to solve your problems, as in reality they can only work on some narrow use cases. Its really just a matter of orchestrating the save in your controllers where the knowledge and the context of what needs to be done belongs.
A strategy I have found works well in json apis is to simply manage relationships on the model that has the foreign key (ie the "belongsTo" side) and avoid returning or managing keys on the hasMany side as all those id's being passed around don't scale well when your collections grow.
It is best to look at the source for the JSONSerializer base class where models are serialized to see what it does, the RESTSerializer inherits its behaviour. You will see that the serializer just saves ids, and does not recursively cascade saves through your object graph, which would be a bad thing as you would be trying to solve the NP-Complete Hamiltonian Path Problem!. This is why I say be VERY suspicious of data layers making promises to solve your model graph persistence problem and instead just orchestrate what you know needs to be done in your controllers. This also aligns very well with dirty tracking and buffering changes (eg using a buffered proxy).
UPDATE
I forgot to add something rather obvious, but you can define your own model serializers that can side-save any related models as part of a single POST/PUT transaction (rather than using a .then approach), depending on what your server supports. Just override the relevant serialize method to include what you need. The EmbeddedRecordsMixin can also be useful if you have some very closely related models that are always created or updated together.
For example, I have Contact model, where each Contact can have one or more Names, Addresses, Emails, or Urls. And each of those are models themselves that track preference/purpose, as well as validFrom/validTo and so on.
Firstly my application serializer mixes in the embedded records support and merging of the attrs property so I can inherit serializers:
// app/serializers/application.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin,{
mergedProperties: [ 'attrs' ]
});
Then my Contact serializer specifies what models are embedded:
// app/serializers/contact.js
import AppSerializer from "./application";
export default AppSerializer.extend({
attrs: {
names: { embedded: 'always'},
addresses: { embedded: 'always' },
emails: { embedded: 'always' },
phones: { embedded: 'always' },
urls: { embedded: 'always' }
}
});
I have an Ember App where some Models use Ember Data and some don't. My question relates to creating relationships between these Models and also the best way to structure the Model relationships.
Models
Currently I have the following Models:
Foods
not using Ember Data
makes $.ajax request to external API
extends a Ember.Object (see here and here for examples of the methodology)
Meals
uses Ember Data
has many Portions
Portions
uses Ember Data
hasOne Meal
hasOne Food
In my app I need a Portion to be a unique record which has a weight field. Each Portion should derive it's other values from a associated Food. A Meal should contain many Portions.
Questions
Should Portions be a Model in it's own right our should it be stored in some kind of array-like structure as a field on the Meal (eg: portions)? Consider that a Portion is not reusable and is only able to be associated with a single Meal.
If "Yes" to #1 then what could my Meal Model def look like?
As Food does not use Ember Data what's the best technique for defining a relationship between a Portion and a Food?
Ultimately the User experience should allow someone to
View a Food
Create a Portion of that Food
Associate the Portion with a Meal
View all Portions associated with a Meal
Your help is much appreciated.
Q1: Should Portions be a Model in it's own right our should it be stored in some kind of array-like structure as a field on the Meal (eg: portions)?
I'm not sure you are asking if Portions should be a model or Portion should be a model. But whatever I think the solution is to build Portion as a model and build portions relationship for Meal model. Because you have functionality to create a portion with a food. In my understanding the portion should be created without a meal (although it can link to a meal later).
Q2: If "Yes" to #1 then what could my Meal Model def look like?
The model definition is like this:
App.Portion = DS.Model.extend({
weight: DS.attr(),
meal: DS.belongsTo('meal', {async: true})
});
App.Meal = DS.Model.extend({
portions: DS.hasMany('portion', {async: true})
});
Q3: As Food does not use Ember Data what's the best technique for defining a relationship between a Portion and a Food?
It's better to still use Ember Data to define Food model, just define your custom adapter and serializer, Ember Data handles the rest. The DS.Adapter and DS.Serializer documentations are good place to start. Below is a simple example.
// Just name it "FoodAdapter" and Ember Data knows to use it for "Food".
App.FoodAdapter = DS.Adapter.extend({
find: function(store, type, id) {
// The returned value is passed to "serializer.extract" then "store.push"
return this._ajax({url: '/external/food', type: 'GET'});
},
createRecord: function() {},
updateRecord: function() {},
deleteRecord: function() {},
findAll: function() {},
findQuery: function() {},
_ajax: function(options) {
// Transform jQuery promise to standard promise
return Em.RSVP.cast($.ajax(options));
}
});
App.FoodSerializer = DS.Serializer.extend({
// Assume the json is:
// {
// "food_data": {
// "name": "XXX",
// "price": 100
// }
// }
extract: function(store, type, payload, id, requestType) {
return payload.food_data;
},
serialize: function() {}
});