I have the following user object (json):
{
"_id": "my-id",
"org": 666,
"type": "user",
"properties": {
"first_name": "Firstname",
"surname1": "Surname1",
"surname2": "Surname2",
"allowed_apps": [ "one-app", "another-app" ],
"default_app": "one-app",
"email": "email#address.com"
},
"outputs": {
"extension": "extension-id"
}
}
This is a single model, with a complex structure. It is not a "multi-model structure". Therefore, I want to define this using DS.Model.extend, but I do not want to use belongsTo relationships for neither properties nor outputs, since they do not refer to other models: these fields are just complex data, direct part of my user object. That is, we do not have any properties model, nor any outputs model. These are just parts of the user model, and as such are stored in the database (couchdb in our case).
Can this be done in ember?
Lamentably this kind of structure is not possible using ember-data models.
Every value of a model that is not a primitive one such as string, number, boolean & date can't be defined as a model property without designing it with belongsTo or hasMany. Furthermore this site jsonapi.org which is still a WIP describes the direction ember-data is going with it's implementation.
So the point is here if you want/need to use ember-data models (DS.Model) your server should obey the JSON format ember-data expects, otherwise you have always the possibility (since ember-data is backend agnostic) to not use ember-data models definition at all, this way your models can be structured the way you want, but then you are out of being helped from the conventional work ember-data adapter and serializer does for you and you have to write your own adapter/s which then deals with all the peculiarities your JSON's have and load them finally into your store.
If you absolutely need a custom data structure to be exchanged with your backend, you can register a custom transform like for example:
App.Adapter.registerTransform('mega', {
deserialize: function(serialized) {
//do your custom deserialization
},
serialize: function(deserialized) {
//do your custom serialization
}
});
and then use it like:
App.MyModel = DS.Model.extend({
megaValue: DS.attr('mega');
});
Hope it helps.
Related
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.
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.
I have an existing search app I built on the server-side that uses elasticsearch processed by python/flask. On the client side, it's using JS/jQuery and handlebars to parse and present the data.
I'd like to take it one step further and build it in ember, as the "controls" (filters, sorting, pagination, etc) make it a perfect candidate for an SPA.
I understand the ember fundamentals, but I feel like I've hit a wall with ember-data - i.e. how to get my data into the store so I can bind actions. Can somebody point me in the right direction?
My existing JSON API is accessed like this:
http://localhost:8000/search?q=shirt&paging=18&filter-price=100
and it returns JSON like this:
{
"aggregations": {
"breadcrumb": [],
"color": [],
"price": [],
"size_apparel": [],
"size_jewelry": []
},
"meta": {
"agg_list": [],
"default_sort": "",
"key_translations": {},
"paging": 18,
"paging_options": [],
"sort_methods": [],
"from": 0,
"hits": 89,
"pages": 5,
"q": "shirt"
},
"results": [
{
"creation_date": "",
"image": "",
"images": {
"altimg1": "",
"large": "",
"regular": "/",
"small": ""
},
"name": "This Product",
"price": "19.95",
"skuid": "ABC123",
"url": "shirt.html"
},
{...},
{...}
]
}
Is this usable or do I need to rewrite the back-end?? I've tinkered with accessing the data and have something very rough working. I can actually see the 'results' data, but I have NO IDEA how to access the 'meta' and 'aggregations' data. I think this is "multiple models in the sam payload" and RSVP.hash should work...but ember-data seems to have a requirement that each model have an id. That makes sense for the "results" model, but not for aggregations and definitely not for meta.
For now I just want to get it to show up.
For starters, my adapter is:
export default DS.RESTAdapter.extend({
host: 'http://localhost:8000',
pathForType() {
return 'search';
}
});
Test controller is:
export default Ember.Controller.extend({
queryParams: ['q','paging'],
q: 'shirt',
paging: '18'
});
my 'result' model is:
const { attr } = DS;
export default Model.extend({
'creation_date': attr('string'),
'has_pricerange': attr('string'),
'image': attr('string'),
'name': attr('string'),
'price': attr('string'),
'skuid': attr('string'),
'url': attr('string')
});
route:
export default Ember.Route.extend({
model(params) {
return this.store.query('result', {
q: params.q,
paging: params.paging
});
}
});
serializer:
export default DS.RESTSerializer.extend({
primaryKey: 'skuid'
});
This is so easy to do with jquery - you just $.get and then access the data with object notation. I know ember is not jquery, but it seems like it should be easier to access data. Am I missing something? Taking the wrong approach? Should I start from scratch?
Ember Data is a great piece of the Ember puzzle, but by no means required. Whether or not you should use Ember Data really depends on the nature of your data, the control of your API, and your desired futureproofability
Nature of your Data
Ember Data really excels at your typical model/entity type data. "Things" that have attributes, and can relate to each other. If your data follows that pattern then Ember Data could be a great fit.
Control of your API
As mentioned in the other answer, you get a lot for free if you can buy into a JSON API format. This has become the standard for Ember Data, but is not exclusive to Ember. A lot of server-side options will have ways to serialize these resources easily with many existing frameworks. If you can't change the data coming from the server, or if it's easier to just handle it on the front-end, you can go down the path of customizing the adapters/serializers in Ember.
Desired Futureproofability
Ember Data allows you to swap out the adapter/serializer while keeping your models in tact. This is desirable if you want your application to handle different sources in the future, maybe swap from using your own API to using a local storage, or a 3rd-party service like firebase. If don't plan on much changing, you can do your basic ajax calls and return a promise within your model hook and Ember will work more or less the same. I would recommend using ember-network which is spec'ed against the fetch API and will likely become more and more supported as a FastBoot compatible request.
Other things to consider is that Ember Data and using the Ember Object can be heavy depending on the amount of instances of each model you'll be passing in.
Others are tackling this problem, too. Toran Billups has pulled the redux pattern with ember-redux, which is another great way of thinking about how you approach your data.
Your use case
So you could:
Use Ember Data with find
Taking a look at the shape of your data, it looks like you're providing more of a search service than querying for specific models/entities. Ember Data does have a find method, but the amount of metadata you're providing it might be overloaded for the model use case.
Forget Ember Data and use search end point
model: function(params), Use the params from the model hook to construct the url to the search end point, and return a promise (add then as needed to shape the data)
Query the search end point (API refactor)
Similar idea as above, but you would use get the product id's and use those to query the ember data store for the products.
Something similar to:
fetch("service-end-point?with=queryparams")
.then(result => {
return {
products: this.store.findMany(Product, result.ids);
};
});
I think it'd be easiest to get started just handling and shaping the data directly in the controller, and skipping Ember Data. If you want specific computed property bindings there's no reason why you can extend Ember.Object with the shape you want and then use something like .map to take the result from the network request and apply it like payload => Object.create(payload).
Let me know if you have any questions surrounding any of these ideas.
I recommend you read this section of the guide, specifically the bit about $.getJSON. Ember is designed to use a powerful store. It's much easier to use if you're using a standard api (e.g. JSON API); you can just use Ember data code directly. But if not, you will have to write some serializer code so Ember knows how to use your api. Once that's done though, your component code won't be coupled to your data fetching code.
You can use $.getJSON if you are still prototyping, though.
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' }
}
});
In my Ember app, I have a nested hasMany relationships using ember-data like so workout->exercise->set.
My API has nested JSON instead of sideloaded JSON, so to fetch an existing workout I use store.find('workout', id) and override extractSingle.
My problem is when building a new workout I need to prepopulate it with exercises and sets based on a workout plan that a user is following. On the server side, I just have a /new controller action that prepopulates everything and renders a template.
Now that I'm moving to Ember I need the same functionality, but can't seem to make it work. The first thing I tried was to use Ember.$.getJSON to call a custom API endpoint in conjunction with pushPayload. This doesn't work however, because pushPayload bypasses extractSingle which means I can't convert my nested JSON into side-loaded JSON.
The prepopulation logic is very complicated so I'd prefer not to duplicated it client side and retrieve it from the API. Any other ideas on how I could accomplish this using ember-data?
For anyone trying to load embedded relationships with EmberData, check out EmberData's EmbeddedRecordsMixin.
Using Ember-CLI, you will need to create a serializer for the model with embedded relationships and add the mixin in the serializer like this:
// app/serializers/my-example-model.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
Then, declare an attrs hash and include each embedded relationship with a hash for each, like this:
attrs: {
conference: { embedded: 'always' },
organizationType: { embedded: 'always' }
}
});
In my example above, conference and organizationType are both embedded in the myExampleModel model for both serializing and deserializing. That means that when I use this serializer over a RESTful API, I will get the embedded objects and I need to put the embedded objects.
The EmbeddedRecordsMixin has several options regarding each relationship, as in you have separate settings for serializing and deserializing. you can specify ids, records, or neither. embedded: always is shorthand for:
{
serialize: 'records',
deserialize: 'records'
}
It's all documented here with better examples here:
http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html.