ember data different models for same endpoint - ember.js

I have an API that doesn't return JSON data in a format that Ember-Data expects. Especially when getting a list of resources versus a single resource.
For example, GET /api/widgets/{id}
Should return a single widget model that might look like this:
//app/models/widget.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
weight: DS.attr('number'),
color: DS.attr('string')
});
Whereas getting the full list of widgets via GET /api/widgets/ returns a model that should look like this:
// app/models/widgetlist.js
import DS from 'ember-data';
export default DS.Model.extend({
total: 22,
widgets: DS.hasMany('widget')
});
(I think that's how my widget list model should look. It's essentially a total count of widgets in system and current paginated set of widgets)
I'm having a really hard time figuring out what combination of models, custom adapter and/or custom serializer I need in order to get this to work.
EDIT:
// Server responses examples
// GET api/widgets/77
{
"id":77,
"name":"Acoustic Twangdoodle",
"weight":3,
"color":"purple"
}
// GET api/widgets/
{
"total":22,
"widgets":[
{
"id":77,
"name":"Acoustic Twangdoodle",
"weight":3,
"color":"purple"
},
{
"id":88,
"name":"Electric Twangdoodle",
"weight":12,
"color":"salmon"
}
]
}

Thats only one model!
Now I don't see how your pagination works. Depending on that maybe you should not use findAll but instead use query to load a paginated set.
The total is not part of the model but of the metadata. Use a custom JSONSerializer and let extractMeta return this.
Depending how your pagination works you wanna do something like store.query('widget', { page: 3 }). If you speak more about how to access page 2 or so it will be easier to explain this.

Related

How can I re-use the same model template for different requests in Ember?

Imagine I have 20 charts on the index page of my application. I can request the data points for each chart in JSON:API form from the API:
export default Route.extend({
model() {
return {
chart01: this.store.findAll('chart-timetable-01'),
chart02: this.store.findAll('chart-timetable-02'),
// ... etc
All routes from the API return a collection of documents that each represent a data point. Every document has the same attributes: name and value.
Now even though they are all exactly the same, I need to have 20 models:
models/chart-timetable-01.js
models/chart-timetable-02.js
# ... etc
Now that I want to introduce a second chart type with an actual different model, I want to simplify this. Can I use one model for all charts that are functionally the same, so that I don't have to duplicate the model for every new chart?
So in stead of chart-timetable-{01..20}.js I can have one simple chart.js model?
I can override the type with a serializer:
import DS from 'ember-data'
export default DS.JSONAPISerializer.extend({
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (primaryModelClass.modelName.match(/^chart-timetable-/g)) {
payload.data.forEach((doc, idx) => doc.type = 'chart')
}
return this._super(...arguments)
}
})
But Ember still wants models named in the findAll to exist. How can I override this?
I'm not sure if I get it correctly but maybe this will help.
Ember provides some helpful called rsvp. You can use it to store all data into return object
import { hash } from 'rsvp';
And then just place whatever You want:
return hash({
chart01: this.store.findAll('chart-timetable-01'),
chart02: this.store.findAll('chart-timetable-02'),
});
And now your model looks like:
chart01{...},
chart02{...}
Hope this will help!

EmberJS 2.7 = has_many configuration for Ember-Data and Active Model Serializers, using Ember-Power-Select (and side loaded, not embedded data)

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 Data usage without a Backend API and converting Models to JSON (Ember 2.5.0)

I am using Ember v2.5.0 without an external datastore.
After creating a record at the route using the createRecord method it cannot be queried for in other parts of the app (controller or components).
My understanding is that I need to use store.push to save the record locally so that it may be accessed by the controller. However the store.push method requires the arguments to be in json format.
I could just do away with the models however I was wondering if there a quick way to convert the models into json format using Ember version 2.5.0?
I would also like to know if my assumptions on using store.push to persist the data locally is a recommended way to go when using Ember Data without an external backend.
There are other references on "Ember models to json" on stack overflow however they are outdated and I particularly would like to know if my approach/assumptions are correct and if not, what the alternatives are. Im very new to Ember.
Problem
//Route
import Ember from 'ember';
export default Ember.Route.extend({
model() {
let shape, square;
square = this.store.createRecord('square');
shape = this.store.createRecord('shape', {
shared: 'shared-value',
square: square
});
return shape;
}
});
//Controller
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
someActionName() {
console.log(this.store.peekRecord('shape', 1)); //undefined!
}
}
});
//Shape Model
import DS from 'ember-data';
export default DS.Model.extend({
shared: DS.attr('string', { defaultValue: '' }),
square: DS.belongsTo('square')
});
//Square Model
import DS from 'ember-data';
export default DS.Model.extend({
sides: DS.attr('string', { defaultValue: '4' }),
whereIbelong: DS.belongsTo('shape')
});
I don't think using store.push is a good long-term approach, it's better to use the save and destroyRecord methods, as Ember Data expects.
You can create an adapter from the base Adapter using local storage, or maybe just returning the record passed in createRecord/updateRecord works.
Experiment with it and find what works better for your use case, adapters are very flexible.
As a side note, the best way I found to make store.push work as expected is like this:
var obj = {
id: '1',
name: "object name",
// More object properties...
};
store.push(store.normalize('model-name', obj));
createRecord does not add an ID. So this.store.peekRecord('shape', 1) is undefined because your record created does not have an ID. Everything works well if you set an ID for your record.
Ember Twiddle: https://ember-twiddle.com/e2b24b5a2cdab19c7b7401c57aff9959?openFiles=controllers.application.js%2C
If your goal is to persist records on client side have a look at ember-local-storage.

Ember 2 simple polymorphic relations

I have a notes model that I want to attach to one of two other models, customers and suppliers.
In my database I have a foreignType and foreignId field that holds the type and the corresponding ID for the customer or supplier, something like
notes: { {id: 1, body:'bar',foreignType:'customer',foreignId:100},
{id: 2, body:'foo',foreignType:'supplier',foreignId:100}
}
That is, a note can be attached to a customer or a supplier.
The convention seems to be that the field be called noteType?
I have seen a tutorial where the related type was nested in the JSON, rather then being at the root.
My ember models look like this:
//pods/note/model.js
export default DS.Model.extend({
//...
body: DS.attr('string'),
foreign: DS.belongsTo('noteable',{polymorphic:true})
});
//pods/noteable/model.js (is there a better/conventional place to put this file?)
export default DS.Model.extend({
notes: DS.hasMany('note')
});
//pods/customer/model.js
import Noteable from '../noteable/model';
export default Noteable.extend({ //derived from Noteable class
name: DS.attr('string'),
//...
});
//pods/supplier/model.js
// similar to customer
// sample incoming JSON
//
{"customer":{"id":2,"name":"Foobar INC",...},
"contacts":
[{"id":1757,"foreignType": "customer","foreignId":2,...},
{"id":1753,"foreignType": "customer","foreignId":2,...},
...],
...
"todos":
[{"id":1,"foreignType":"customer","foreignId":2,"description":"test todo"}],
"notes":
[{"id":1,"foreignType":"customer","foreignId":2,"body":"Some customer note "}]
}
How to set this up correctly, i.e. what does Ember expect?
My notes aren't attaching correctly to the customer model. They show up in the Data tab of the Ember Inspector, but the notes list of any customer is empty.
I can see several possibilities:
extend customer/supplier from DS.Model and have a property notes: belongsTo('noteable'), that would mean the belongsTo in notes isn't polymorphic, as there wouldn't be any derived classes, only Noteable itself. Not sure if ember (data) can deal with this nesting correctly.
extend from Noteable. what if I want to have other things like addresses or contacts, that can be related to customer or supplier?
create duplicate models like customernote/suppliernote, customercontact/ suppliercontact, customer/supplier/employee address. And have the backend return the filtered table/model name depending on the endpoint. I don't like to repeat myself though ....
Ember : 2.2.0
Ember Data : 2.2.1
I love how Ember doc has explained polymorphism here - https://guides.emberjs.com/v2.13.0/models/relationships/#toc_polymorphism
So, first you need to have a 'type' which will define the model to be used (your data calls it foreignType)
Next, your note model will be the polymorphic model (similar to paymentMethod model in the example above). Let me know in the comment if you need more clarification, but I think if you follow the given example, it'll be very clear.

Ember-cli deserialize array error

I've a problem deserializing an array with ember-cli and ember data.
I've models such as:
Label=DS.Model.extend
..
days:DS.hasMany('day')
Day = DS.Model.extend
hours: DS.attr()
The received JSON is:
labels:[
{
id: 1
days:[{
hours: [1,10, 33, 44,55,21]
}]
}
]
Now: I can properly manage Embeddedrecord with EmbeddedREcordMixin but whenever an hours array is deserialized it is transformed into something like:
[0,0,0,0,1,0,0,0,1,0,0,0,0]
removing all original values.
I tryed defining a specific transform, or changing the relationship to async and normalizing the payload in a specific labelSerializerbut nothing seems to have effect and I wasn't able to identify where the array is actually modified..
Solution
Finally, it was a problem of the received data set. Some record, with the same id was overwriting others, creating that misleading result.
With EmberData 1.0.0.beta8, no ArrayTransform is needed. Just plain DS.attr() did the job.
EDIT:
I've tried the same implementation on a non ember-cli application and it worked fine. I used EmbeddedRecordsMixin in the LabelSerializer to handle day embedded hasMany relation, as well as customized the normalizePayload and extractArray functions in order to fix small problems with ids etc..
But I'm quite new to Ember-cli, I'm not sure if I'm missing something. There is a need of special configurations in order to use ActiveModelAdapter, and the EmbeddedRecordsMixin?
EDIT2
Invalidate all about edit1...
EDIT3
After more testings, it is not a problem of Ember-cli.
When I test the deserialization through store.pushPayload() any manipulations of the JSON done in the labelSerializer#normalizePaylod or labelSerializer#extractArry works as expected.
Instead, when connecting to a remote server, the result array of values is a set of 0 and 1s.
I had a similar problem with Ember-Cli and Ember Data. I solved it like this:
I created a file called array.js in the transforms folder of my Ember-cli project.
(you can call the file what ever you like)
transforms/array.js
import DS from 'ember-data';
export default DS.Transform.extend({
deserialize: function(serialized) {
return serialized;
},
serialize: function(deserialized) {
return deserialized;
}
});
Then in my model, I did this:
somemodel.js
import DS from 'ember-data';
export default DS.Model.extend({
listOfSomething: DS.attr('array') //same name as the transform-file you created
});
Check out the Ember CLI documentation Using modules and the resolver
Edit
After taking a close look at your example, I see that the JSON is not on the correct format that Ember expects. If you check out the Ember Documentation on relationship between models your JSON should look something like this:
labels:[
{
id: 1
days:[1]
}
]
days: [
{
id: 1,
hours: [1, 10, 33, 44, 55, 21]
}
]