Customize JSON collection name in Serializer - ember.js

When accessing API resources like /api/users/ which list/search a resource Ember Data expectects the response to be in this format:
{
"users": [{
"name": "Rails"
}, {
"name": "Omakase"
}]
}
but my response looks like this:
{
"results": [{
"name": "Rails"
}, {
"name": "Omakase"
}]
}
How can I tell my Serializer to turn results into users?
The following Serialzer renames single attributes but not the whole list as shown above:
import DS from 'ember-data';
export default DS.ActiveModelSerializer.extend({
attrs: {
"users" : "results",
}
});

this should do it for you, dont change attributes, just these two methods for extracting single models and arrays of models. You take the payload.results property and process it further, instead of the default payload.
extractArray: function(store, type, payload) {
return Array.prototype.map.call(payload.results, function(hash) {
return this.normalize(type, hash, payload.type);
}, this);
},
extractSingle: function(store, type, payload, recordId) {
return this.normalize(type, payload.results, payload.type);
}

Related

Ember Data Serializer & Adapter with Supabase returns empty (Proxy { })

Struggling to create my customised adapter & serializer to integrate Supabase, how I'm stuck why no data in Ember Data.
Trying out with a simple findAll() method. See below:
Service ⬇️:
export default class SupabaseService extends Service {
client;
constructor() {
super(...arguments);
const { url, key } = ENV.supabase;
const supabase = createClient(url, key);
this.client = supabase;
}
}
Model ⬇️:
export default class CourseModel extends Model {
#attr('string') name;
#attr('date') date_added;
}
Adapter ⬇️:
export default class ApplicationAdapter extends RESTAdapter {
#service supabase;
async findAll(store, type, neverSet, snapshotRecordArray) {
return new Promise(async (resolve, reject) => {
try {
const { data, error, status } = await this.supabase.client
.from(pluralize(type.modelName))
.select('*');
if (error) {
reject(error);
} else {
resolve(data);
}
} catch (error) {
reject(error);
}
});
}
}
Serializer ⬇️:
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
// parse the response data from the server and return it in the format that Ember Data expects
let newPayload = {
data: payload.map(item => {
let attributes = JSON.parse(JSON.stringify(item));
delete attributes.id;
return {
id: item.id,
type: primaryModelClass.modelName,
attributes: attributes
}
})
}
return super.normalizeResponse(store, primaryModelClass, newPayload, id, requestType);
}
✅ The service works fine. The adapter manage to get data and returns as follows:
[
{
"id": "259f46fd-3321-4cc9-ad5e-6d6ec880f7f1",
"date_added": "2022-12-31T00:03:14.618585+00:00",
"name": "Science"
},
{
"id": "62a6a085-604b-4600-8cc4-59a8c9af284a",
"date_added": "2022-12-31T00:03:30.010963+00:00",
"name": "Physics"
}
]
The serializer newPayload to follow JSON API schema, returns:
{
"data": [
{
"id": "259f46fd-3321-4cc9-ad5e-6d6ec880f7f1",
"type": "course",
"attributes": {
"name": "Science",
"date_added": "2022-12-31T00:03:14.618585+00:00"
}
},
{
"id": "62a6a085-604b-4600-8cc4-59a8c9af284a",
"type": "course",
"attributes": {
"name": "Physics",
"date_added": "2022-12-31T00:03:30.010963+00:00"
}
}
]
}
But the problem is no data in store. Logging model in template shows empty Proxy {}.
I have no idea why. Ember Inspector shows no model in Data.
Any suggestions?

"str is undefined" on findRecord

I have a problem with model and serializer. I've looked at the documentation and can't find a solution. My app is connected to a django rest api.
Django response:
{
"id": 1,
"url": "http://localhost:8000/users/1/",
"username": "username",
"email": "xxx#email.com",
"is_staff": true
}
The model:
export default DS.Model.extend({
url : DS.attr('string'),
username : DS.attr('string'),
email : DS.attr('string'),
});
I've modified the response with a serializer to include "data":
export default DS.JSONAPISerializer.extend({
primaryKey: 'id',
normalizeFindRecordResponse(store, type, payload, id) {
console.log('payload',payload)
return {
data: {
'id': id,
'url': payload.url,
'username': payload.username,
'email': payload.email,
}
} ;
}
});
The route:
export default Ember.Route.extend({
model() {
return this.store.findRecord('users', 1);
}
});
Another version of the route same error:
export default Ember.Route.extend({
model() {
this.store.findRecord('users', '1').then(function(user){
console.log('user', user);
}).catch(function(e){
console.log('e', e);
});
}
});
Finally Ember inspector has the model but all values as undefined and "str is undefined" on console.
Image of ember inspector
1.normalizeFindRecordResponse is not returning valid JSONAPI format,
The below is the valid format,
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
// ... this article's attributes
}
}
}
As you can see, you are missing type, attributes key.
2.Your model file name should be user.js in singular.
3.this.store.findRecord('users', 1) - here you need to use singluar form of the model so this.store.findRecord('user', 1)
4.In your another attempt you are missing the return statement in model hook.

ember data no model was found for attribute name

I have defined a model(app/models/job.js)
import DS from 'ember-data';
export default DS.Model.extend({
status: DS.attr(),
result: DS.attr()
});
And I am trying to load it from the index controller(app/controllers/index.js)
import Ember from 'ember';
export default Ember.Controller.extend({
productName: "",
customerName: "",
startDate: "",
endDate: "",
actions: {
search: function() {
let data = this.store.find("job", '9e5ce869-89b3-4bfc-a70f-034593c21eae');
return data;
}
}
});
The HTTP response I get is:
{
"status": "OK",
"result": {
"b": 2,
"a": 2,
"see": 1,
"c": 1
}
}
How ever I get following error and warning:
WARNING: Encountered "status" in payload, but no model was found for model name "status" (resolved model name using next-gen-analytics#serializer:job:.modelNameFromPayloadKey("status"))
WARNING: Encountered "result" in payload, but no model was found for model name "result" (resolved model name using next-gen-analytics#serializer:job:.modelNameFromPayloadKey("result"))
TypeError: Cannot read property '_internalModel' of undefined
at finders.js:50
at Object.Backburner.run (ember.debug.js:224)
at ember$data$lib$system$store$$Service.extend._adapterRun (store.js:2043)
at finders.js:45
at tryCatch (ember.debug.js:56151)
at invokeCallback (ember.debug.js:56166)
at publish (ember.debug.js:56134)
at ember.debug.js:32577
at Queue.invoke (ember.debug.js:910)
at Object.Queue.flush (ember.debug.js:974)onerrorDefault # ember.debug.js:32616exports.default.trigger # ember.debug.js:56792Promise._onerror # ember.debug.js:57758publishRejection # ember.debug.js:56065(anonymous function) # ember.debug.js:32577Queue.invoke # ember.debug.js:910Queue.flush # ember.debug.js:974DeferredActionQueues.flush # ember.debug.js:770Backburner.end # ember.debug.js:160Backburner.run # ember.debug.js:228run # ember.debug.js:20238ember$data$lib$system$adapter$$default.extend.ajax.Ember.RSVP.Promise.hash.success # rest-adapter.js:831jQuery.Callbacks.fire # jquery.js:3148jQuery.Callbacks.self.fireWith # jquery.js:3260done # jquery.js:9314jQuery.ajaxTransport.send.callback # jquery.js:9718
Any suggestion will be appreciated
UPDATE
I was thinking this is kind of a bug, so I went ahead to log a bug on ember-data github repo, got the response from #wecc in an hour or so (NICE)
https://github.com/emberjs/data/issues/3683
So to fix this issue, I wrote my own serializer.
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizePayload: function(payload) {
return {
'job': {
id: '9e5ce869-89b3-4bfc-a70f-034593c21eae',
status: payload.status,
result: payload.result
}
};
}
});
And now, it starts working.
OPINION I can think of why they implemented the default RESTSerializer this way, but we probably should give more information to the users on the documentation of the ember-data, otherwise, newbie who is trying to use it will get lost.
1) Your server response should have a root element with the same name, as model's. So, for single object it shuld be:
{
"job": {
"id": "<backend must provide an id, integer preferred>"
"status": "OK",
"result": {
"b": 2,
"a": 2,
"see": 1,
"c": 1
}
}
And for multiple objects:
{
"jobs": [{
"id": "<backend must provide an id, integer preferred>"
"status": "OK",
"result": {
"b": 2,
"a": 2,
"see": 1,
"c": 1
}, {/*next object*/}]
}
I tried to debug it, and I found that the code goes to rest-serializer.js and extractSingle function, and goes to the line "if (!store.modelFactoryFor(modelName)) {" and it returns "false".
extractSingle: function (store, primaryTypeClass, rawPayload, recordId) {
Ember.deprecate("`serializer.normalizePayload` has been deprecated. Please use `serializer.normalizeResponse` with the new Serializer API to modify the payload.", this.normalizePayload === JSONSerializer.prototype.normalizePayload, {
id: "ds.serializer.normalize-payload-deprecated",
until: "2.0.0"
});
var payload = this.normalizePayload(rawPayload);
var primaryRecord;
for (var prop in payload) {
var modelName = this.modelNameFromPayloadKey(prop);
if (!store.modelFactoryFor(modelName)) {
Ember.warn(this.warnMessageNoModelForKey(prop, modelName), false, {
id: "ds.serializer.model-for-key-missing"
});
continue;
}
var isPrimary = this.isPrimaryType(store, modelName, primaryTypeClass);
var value = payload[prop];

Time for pushPayload? Saving records to ember-data

Edited as I narrowed down issues....
I'm working on an app that is taking in an API - data as JSON. It is an array of groups, with "name", "id", and other such elements.
{
"status": "success",
"data": {
"groups": [
{
"id": 7100,
"name": "Test 12345",
"kind": "floor",
"parent_group_id": 7000,
"controlled_device_type_count": {},
"is_top_level": true
}
]
}}
I also have a livestream websocket - data as JSON stream. It should update the elements referenced in the first API. The two only share "id".
Livestream:
{
"group":{
"usage":{
"10":1,
"20":0,
"30":2,
"40":2
},
"last_change":"2014-03-24T05:56:10Z",
"id":7954
}}
**Updated...**My IndexRoute:
App.ApplicationAdapter = DS.RESTAdapter.extend({
extractArray: function(store, type, payload, id, requestType) {
payload = payload.data;
return this._super(store, type, payload, id, requestType);
}
});
App.IndexRoute = Ember.Route.extend({
sortProperties: ['id'],
sortAscending: true,
beforeModel: function() {
var socket = window.io.connect('http://localhost:8887');
var self = this;
socket.on('group_live_stream', function(data){
var dataObj = JSON.parse(data);
self.store.push('group',dataObj.group);
});
},
actions: {
toggleMenu: function() {
this.controller.toggleProperty('menuVisible');
this.controller.pushBody();
} },
activate: function() {
var self = this;
$.getJSON('http://localhost:3000/api/groups/top?subscribe=true').then(function(data) {
self.store.pushMany('group', data.data.groups);
});
},
model: function() {
return this.store.all('group');
}
});
Updated:
So now I see the livestream coming through - and for a brief, immediate second - I see the API data (group name) and then it disappears. I'm thinking it's because I'm just "pushMany"-ing records and deleting old ones instead of updating. I've heard/read that pushPayload might be my solution....but I can't figure it out. At all (and when I put it in, I just get an error: "Uncaught Error: No model was found for '0' " Help?!
Any thoughts?
Thanks so much!
ptep
Your API's payload is not in the format that ember-data expects (it looks for your payload in the root of your JSON by default). You'll need to override extractArray (and likely extractSingle) on your ApplicationAdapter something like this:
ApplicationAdapter = DS.RESTAdapter.extend({
extractArray: function(store, type, payload, id, requestType) {
payload = payload.data;
return this._super(store, type, payload, id, requestType);
}
});

Ember.js and API pagination

I'm using Ember.js (v1.2.0) with an API which returns paginated JSON data like this:
{
"count": 5,
"next": "http://127.0.0.1:8000/some/resource/?page=2",
"previous": null,
"results": [
{
"id": 37,
"title": "Some title",
"description": "Some description",
},
{
"id": 35,
"title": "Sdflskdf",
"description": "sdfkdsjf",
},
{
"id": 34,
"title": "Some other title",
"description": "Dsdlfksdf",
},
]
}
I'm not using ember-data, so I'm using a plain ember object as my model and loading the data like this:
App.SomeResource = Ember.Object.extend({});
App.SomeResource.reopenClass({
find: function () {
return $.getJSON('/some/resource/').then(function (response) {
return response.results.map(function (data) {
return App.SomeResource.create(data);
});
});
},
});
The find method on my model class returns a promise which resolves to an array of objects. While creates SomeResource objects, all the pagination data is lost.
Is there a way to store count, next and previous page urls somewhere when the promise resolves?
I am assigning them to global object but you should do better.
App.SomeResource = Ember.Object.extend({});
App.SomeResource.reopenClass({
find: function () {
return $.getJSON('/some/resource/').then(function (response) {
return RSVP.all(response.results.map(function (data) {
return App.SomeResource.create(data);
})).then(function(createdResources) {
window.count = response.count;
window.next = response.next;
window.previous = response.previous;
return createdResources;
});
});
}
});
Rather than storing this metadata on the global window object, I came up with this:
App.SomeResource.reopenClass({
find: function () {
var url = '/some/resource/';
return Ember.$.getJSON(url).then(function (response) {
response.results = response.results.map(function (resource) {
return App.SomeResource.create(resource);
});
return response;
});
},
});
SomeResource.find() just instantiates Ember objects from the results array and then returns the response with the rest of the data untouched. The route then receives the response and sets up the pagination data and the model in the setupController function:
App.SomeResourceRoute = Ember.Route.extend({
model: function () {
return App.SomeResource.find();
},
setupController: function(controller, model) {
controller.setProperties({count: model.count,
pageCount: model.page_count,
currentPage: model.current_page,
pageRange: model.page_range,
previous: model.previous,
next: model.next,
model: model.results,});
},
});
It works, but maybe there is a better way.