serializer in unit test doesn't process json - unit-testing

the serializer in unit test is not processing json at all, but it works as expected in the application. Yeah, I wrote it afterwards, but the question is - why it's not working? I tried also to create it in place, inherit from RESTSerializer, create models in place, but none of that worked.
Maybe someone can give a clue?
update
looks like everything begins in the
isPrimaryType: function (store, typeName, primaryTypeClass) {
var typeClass = store.modelFor(typeName);
return typeClass.modelName === primaryTypeClass.modelName;
},
last string returns false, because of primaryTypeClass.modelName is undefined
Serializer unit test
import DS from 'ember-data';
import { moduleForModel, test } from 'ember-qunit';
import setupStore from 'app/tests/helpers/setup-store';
import Profile from 'app/models/profile';
import Email from 'app/models/email';
import Address from 'app/models/address';
import ProfileSerializer from 'app/serializers/profile';
var env;
moduleForModel('profile', 'Unit | Serializer | profile', {
needs: ['serializer:profile', 'serializer:email', 'serializer:address', 'model:contactable', 'model:email', 'model:address'],
beforeEach: function () {
env = setupStore({
profile: Profile,
email: Email,
address: Address
});
env.registry.register('serializer:profile', ProfileSerializer);
env.profileSerializer = env.container.lookup('serializer:profile');
},
teardown: function() {
Ember.run(env.store, 'destroy');
}
});
test('it converts embedded records attributes', function(assert) {
// expect(3);
let payload = {
id: 1,
first_name: "Carlo",
last_name: "Schuppe",
company: "Metz-Witting",
birthday: "01-10-1985",
photo: null,
emails: [{address: "foo#bar.baz", id: 1, type: "main"}],
addresses: [{city: "Brooklyn", id: 1, type: "main"}]
},
parsed = {
"data":
{
"id":"1",
"type":"profile",
"attributes": { "firstName":"Carlo","lastName":"Schuppe","company":"Metz-Witting","birthday":"01-10-1985","photo":null },
"relationships": {
"emails": { "data": [{"id":"1","type":"email"}] },
"addresses": { "data": [{"id":"1","type":"address"}] }
}
},
"included":[
{"id":"1","type":"email","attributes":{"address":"foo#bar.baz", "kind": "main"},"relationships":{"contactable":{"data":{"type":"profile","id":"1"}}}},
{"id":"1","type":"address","attributes":{"city":"Brooklyn", "kind": "main"},"relationships":{"contactable":{"data":{"type":"profile","id":"1"}}}}
]
},
find, update, findAllRecordsJSON;
Ember.run(function() {
find = env.profileSerializer.normalizeResponse(env.store, Profile, payload, '1', 'findRecord');
// update = env.profileSerializer.normalizeResponse(env.store, Profile, payload, '1', 'updateRecord');
// findAllRecordsJSON = env.profileSerializer.normalizeResponse(env.store, Profile, payload, '1', 'findAll');
});
assert.deepEqual(find, parsed);
// assert.deepEqual(update, parsed);
// assert.deepEqual(findAllRecordsJSON, parsed);
});
setup_store.js
import Ember from 'ember';
import DS from 'ember-data';
// import ActiveModelAdapter from 'active-model-adapter';
// import ActiveModelSerializer from 'active-model-adapter/active-model-serializer';
export default function setupStore(options) {
var container, registry;
var env = {};
options = options || {};
if (Ember.Registry) {
registry = env.registry = new Ember.Registry();
container = env.container = registry.container();
} else {
container = env.container = new Ember.Container();
registry = env.registry = container;
}
env.replaceContainerNormalize = function replaceContainerNormalize(fn) {
if (env.registry) {
env.registry.normalize = fn;
} else {
env.container.normalize = fn;
}
};
var adapter = env.adapter = (options.adapter || '-default');
delete options.adapter;
if (typeof adapter !== 'string') {
env.registry.register('adapter:-ember-data-test-custom', adapter);
adapter = '-ember-data-test-custom';
}
for (var prop in options) {
registry.register('model:' + Ember.String.dasherize(prop), options[prop]);
}
registry.register('store:main', DS.Store.extend({
adapter: adapter
}));
registry.optionsForType('serializer', { singleton: false });
registry.optionsForType('adapter', { singleton: false });
registry.register('adapter:-default', DS.Adapter);
registry.register('serializer:-default', DS.JSONSerializer);
registry.register('serializer:-rest', DS.RESTSerializer);
registry.register('serializer:-rest-new', DS.RESTSerializer.extend({ isNewSerializerAPI: true }));
registry.register('adapter:-active-model', DS.ActiveModelAdapter);
registry.register('serializer:-active-model', DS.ActiveModelSerializer.extend({isNewSerializerAPI: true}));
registry.register('adapter:-rest', DS.RESTAdapter);
registry.injection('serializer', 'store', 'store:main');
registry.register('transform:string', DS.StringTransform);
registry.register('transform:number', DS.NumberTransform);
registry.register('transform:date', DS.DateTransform);
registry.register('transform:main', DS.Transform);
env.serializer = container.lookup('serializer:-default');
env.restSerializer = container.lookup('serializer:-rest');
env.restNewSerializer = container.lookup('serializer:-rest-new');
env.store = container.lookup('store:main');
env.adapter = env.store.get('defaultAdapter');
env.registry.register('serializer:-active-model', DS.ActiveModelSerializer.extend({isNewSerializerAPI: true}));
env.registry.register('adapter:-active-model', DS.ActiveModelAdapter);
env.registry.register('serializer:application', DS.ActiveModelSerializer.extend({isNewSerializerAPI: true}));
return env;
}
output
{
"data": null,
"included": []
}

Related

Mirage server GETs data but POST fails

I have the mirage models:
// mirage/models/country.js
import { Model, belongsTo, hasMany } from 'miragejs';
export default Model.extend({
name: '',
iso3166_1_alpha3: '',
capitol_city: belongsTo('city', {inverse: null}),
cities: hasMany('city', {inverse: 'country'})
});
and:
// mirage/models/city.js
import { Model, belongsTo } from 'miragejs';
export default Model.extend({
name: '',
country: belongsTo('country', {inverse: 'cities'})
});
and the serializer:
// mirage/serializers/application.js
import { camelize, capitalize, underscore } from '#ember/string';
import { JSONAPISerializer } from 'miragejs';
export default class ApplicationSerializer extends JSONAPISerializer
{
alwaysIncludeLinkageData = true;
keyForAttribute(attr) {
return underscore(attr);
};
keyForRelationship(modelName) {
return underscore(modelName);
};
typeKeyForModel(model) {
return capitalize(camelize(model.modelName));
};
};
When I run the tests:
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Unit | Mirage | mirage models', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
test('it retrieves the country', async function (assert) {
const server = this.server;
let city = server.create('city', { id: '1', name: 'Paris' });
server.create(
'country',
{
id: 'FR',
name: 'France',
iso3166_1_alpha3: 'FRA',
capitol_city: city
}
);
let response = await fetch('/api/countries')
assert.strictEqual(response.status, 200, "Should have created the model");
let json = await response.json();
assert.deepEqual(
json,
{
data: [
{
type: 'Country',
id: 'FR',
attributes: {
name: 'France',
iso3166_1_alpha3: 'FRA',
},
relationships: {
capitol_city: {data: {type: 'City', id: '1'}},
cities: {data: []},
}
}
]
}
)
});
test('it creates the country', async function (assert) {
const server = this.server;
server.create('city', { id: '1', name: 'Paris' });
let response = await fetch(
'/api/countries',
{
method: 'POST',
headers: {'Countent-Type': 'application/json'},
body: JSON.stringify(
{
data: {
id: 'FR',
type: 'Country',
attributes: {
iso3166_1_alpha3: 'FRA',
name: 'France',
},
relationships: {
capitol_city: { data: { type: 'City', id: '1'} },
cities: { data: [{ type: 'City', id: '1'}] }
}
}
}
)
}
);
console.log((await response.json()).message);
assert.strictEqual(response.status, 201, "Should have created the model");
});
});
The first one passes and the second one fails with the message:
Mirage: You're passing the relationship 'capitol_city' to the 'country' model via a POST to '/api/countries', but you did not define the 'capitol_city' association on the 'country' model.
How can I get Mirage to recognise the capitol_city attribute on the model?
Mirage is opinionated with regards to the format of attributes and expects the attributes to be in camelCase (and not snake_case).
Unfortunately the Ember CLI Mirage model relationships documentation does not mention this expectation and all the examples use single-word attributes. Even more unfortunately, Mirage will work with snake_case attributes for simple GET requests and when directly creating models through the API; it is only when you make a request to POST/PUT/PATCH a model into the server that it fails and the message will (confusingly) refer to the snake case attribute which has been defined. (See the Mirage source code for where it fails.)
To solve it, convert the attributes to camel case:
// mirage/models/country.js
import { Model, belongsTo, hasMany } from 'miragejs';
export default Model.extend({
name: '',
iso31661Alpha3: 0,
capitolCity: belongsTo('city', {inverse: null}),
cities: hasMany('city', {inverse: 'country'})
});
and change it in the tests as well:
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Unit | Mirage | mirage models', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
test('it retrieves the country', async function (assert) {
const server = (this as any).server;
let city = server.create('city', { id: '1', name: 'Paris' });
server.create(
'country',
{
id: 'FR',
name: 'France',
iso31661Alpha3: 'FRA',
capitolCity: city
}
);
let response = await fetch('/api/countries')
assert.strictEqual(response.status, 200, "Should have created the model");
let json = await response.json();
console.log(JSON.stringify(json));
assert.deepEqual(
json,
{
data: [
{
type: 'Country',
id: 'FR',
attributes: {
name: 'France',
iso3166_1_alpha3: 'FRA',
},
relationships: {
capitol_city: {data: {type: 'City', id: '1'}},
cities: {data: []},
}
}
]
}
)
});
test('it creates the country', async function (assert) {
const server = (this as any).server;
let city = server.create('city', { id: '1', name: 'Paris' });
let response = await fetch(
'/api/countries',
{
method: 'POST',
headers: {'Countent-Type': 'application/json'},
body: JSON.stringify(
{
data: {
id: 'FR',
type: 'Country',
attributes: {
iso3166_1_alpha3: 'FRA',
name: 'France',
},
relationships: {
capitol_city: { data: { type: 'City', id: '1'} },
cities: { data: [{ type: 'City', id: '1'}] }
}
}
}
)
}
);
console.log((await response.json()).message);
assert.strictEqual(response.status, 201, "Should have created the model");
});
});
However, once you convert it to camel case then the attribute iso31661Alpha3 does not get formatted correctly in the output so you have to manually change the serializer for the country model:
// mirage/serializers/country.js
import ApplicationSerializer from './application';
export default class CountrySerializer extends ApplicationSerializer
{
keyForAttribute(attr: string) {
switch(attr)
{
case 'iso31661Alpha3': return 'iso3166_1_alpha3';
default: return super.keyForAttribute(attr);
}
};
};
Once the attributes are in the correct case then it will work.

How can I mock a paginated GraphQL query?

I am using apollo/client and graphql-tools/mock to auto mock graphql queries and test React Native components that use them. My schema is generated from an introspection query created by graphql-codegen. For the most part, my queries are getting mocked by addMocksToSchema just fine. However I have a query that is not returning any mock data.
The query is paginated and doesn't follow the same structure of the examples in the docs (https://www.graphql-tools.com/docs/mocking). Instead of having a query with a node that has a field that is a connection type, the connection is returned from the query. This means I can't use relayStylePaginationMock to mock my function because the resolver argument of addMocksToSchema expects the nodes to be objects not functions(function is the return type of relayStylePaginationMock).
In the below code I have tried overriding the newsPost query with a resolver, but I can't figure out how to get the NewsPostEdges from the store and put them in my mock. Everything I have tried has broken the mock and caused it to return undefined for the whole mocked query.
Why does a paginated mock not work by default?
How can I mock this query?
Schema:
type Query {
newsPost: NewsPostConnection
}
type NewsPostConnection {
totalCount: Int
edges: [NewsPostEdge]!
pageInfo: PageInfo!
}
type NewsPostEdge {
node: NewsPostNode
cursor: String!
}
type NewsPostNode {
newsPostId: Int!
isPinned: Boolean!
label: String
title: String
content: String
postType: NewsPostType!
createdDate: DateTime
createdDateTime: String
creator: UserNode!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
endCursor: String
startCursor: String
}
News Posts query:
query NewsPosts(
$after: String
$first: Int
$newsPostId: Filter_ID
$sort: [NewsPostSortEnum]
$isPinned: Filter_Boolean
) {
newsPosts(
after: $after
first: $first
newsPostId: $newsPostId
sort: $sort
isPinned: $isPinned
) {
pageInfo {
hasNextPage
endCursor
}
edges {
post: node {
newsPostId
postType
isPinned
label
createdDateTime
creator {
initials
avatarUrl
displayName
}
content
}
}
}
}
newsPostsContent.test.tsx
import React from 'react';
import { waitFor } from '#testing-library/react-native';
import { PartialDeep } from 'type-fest';
import { faker } from '#faker-js/faker';
import { createFakeUser, render } from '#root/unit-tests/#util';
import { NewsPostNode, NewsPostType } from '#root/src/generated';
import NewsPostContent from '../NewsPostContent';
const mocks = {
NewsPostNode: (): PartialDeep<NewsPostNode> => {
const postId = faker.random.numeric(4);
const createdDate = faker.date.recent(10);
return {
postId,
isPinned: true,
label: 'test',
content: `<div><p>${faker.random.words(10)}</p></div>`,
postType: NewsPostType.Announcement,
createdDate: createdDate.toISOString(),
createdDateTime: createdDate.toISOString(),
};
},
UserNode: createUserPerson(),
};
describe('Dashboard News', () => {
it('renders dashboard news', async () => {
const { getByTestId, debug } = render(
<NewsPostContent />,
mocks,
);
await waitFor(() => [debug(), expect(getByTestId('newsPostContent:Card')).toBeDefined()]);
});
});
NewsPostsContetnt.tsx
const NewsPostContent = () => {
const [newsPostList, setNewsPostList] = useState<PartialDeep<NewsPostNode>[]>([])
const {
data,
loading,
refetch: refetchPosts,
} = useNewsPostsQuery({
variables: { first: MAX_POSTS, isPinned: true, sort: [PostSortEnum.CreatedDateDesc] },
});
console.log(data); // <-- returns undefined when mock breaks
useEffect(() => {
const newsPostEdges = data?.newsPosts?.edges ?? [];
const newsPostNodes = newsPostEdges.reduce((posts, newsPostNode) => {
if (newsPostNode?.post) {
posts.push(newsPostNode.post);
}
return posts;
}, [] as PartialDeep<NewsPostNode>[]);
setNewsPostList(newsPostNodes);
}, [data]);
return (
{<View>
// Component UI to render posts
</View>}
)
}
AutoMockedProvider.tsx
import React from 'react';
import { ApolloProvider, ApolloClient, InMemoryCache } from '#apollo/client';
import { buildClientSchema } from 'graphql';
import {
addMocksToSchema,
createMockStore,
IMocks,
IMockStore,
relayStylePaginationMock,
} from '#graphql-tools/mock';
import { SchemaLink } from '#apollo/client/link/schema';
import { faker } from '#faker-js/faker';
const introspectionResult = require('../../src/generated/introspection.json');
const defaultMocks = {
Date: () => faker.date.recent().toISOString(),
DateTime: () => faker.date.recent().toISOString(),
};
const resolvers = (store: IMockStore) => ({
Query: {
newsPosts: (root, { isPinned, after, first, postId, sort }) => {
return {
edges: (ref) => {
const connectionsRef = store.get('NewsPostConnection');
const edgesRef = store.get(connectionsRef, 'edges');
return edgesRef; // <-- this breaks the mock
},
pageInfo: {
endCursor: null,
hasNextPage: false,
},
};
},
},
});
const AutoMockedProvider = ({
mocks = {},
children,
}: React.PropsWithChildren<{ mocks?: IMocks }>) => {
const schema = buildClientSchema(introspectionResult);
const store = createMockStore({ mocks: { ...defaultMocks, ...mocks }, schema });
const schemaWithMocks = addMocksToSchema({
schema,
mocks: {
...defaultMocks,
...mocks,
},
resolvers,
preserveResolvers: false,
store,
});
const client = new ApolloClient({
link: new SchemaLink({ schema: schemaWithMocks }),
cache: new InMemoryCache(),
});
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
export default AutoMockedProvider;

Need some guidance on Ember component functionality

I'm currently leveraging ember-cli-geolocate along with ember-google-maps to provide users with the closest point of interest to their current location. I've got the code working in a component but have now realized that I'm not able to sort.
Code below:
//routes/vineyard/index.js
import Ember from 'ember';
import { later } from '#ember/runloop';
import { inject as service } from '#ember/service';
import $ from 'jquery';
const { Route, set } = Ember;
export default Route.extend({
model() {
return this.store.findAll('vineyard');
},
setupController(controller, model) {
set(controller, 'vineyards', model);
},
activate() {
this.controllerFor('vineyard/index').send('distanceFrom');
}
});
//controllers/vineyard/index.js
import Ember from 'ember';
import { inject as service } from '#ember/service';
import $ from 'jquery';
export default Ember.Controller.extend({
userLocation: null,
endLocation: null,
milesAway: null,
locationIsLoading: true,
failState: false,
googleMapsApi: service(),
geolocation: service(),
panelActions: Ember.inject.service(),
userLocationChanged: function () {
this.get('userLocation');
this.toggleProperty('locationIsLoading');
}.observes('userLocation'),
actions: {
distanceFrom: function() {
this.get('geolocation').trackLocation().then((geoObject) => {
let currentLocation = this.get('geolocation').get('currentLocation');
this.set('userLocation', currentLocation);
}, (reason) => {
// this.toggleProperty('failState');
// $('.error').css('height', '220px');
// $('.error > p').css('height', 'auto');
console.log('Geolocation failed because ' + reason);
});
},
stopError: function() {
this.toggleProperty('failState');
$('.error').css('height', '0');
$('.location-loader').animate({opacity: '0'}, 1000);
}
},
});
components/miles-away.js
import Component from '#ember/component';
import { inject as service } from '#ember/service';
import { later } from '#ember/runloop';
import $ from 'jquery';
export default Component.extend({
googleMapsApi: service(),
geolocation: service(),
userLocation: null,
endLocation: null,
milesAway: null,
distanceLoading: true,
errorState: false,
fadeClass: '',
didInsertElement() {
this.set('self', this);
var address = this.get('address');
var location = this.get('location');
var distanceLoading = this.get('distanceLoading');
var userLocationLat = location[0];
var userLocationLon = location[1];
let userLocation = '' + userLocationLat + ',' + userLocationLon
this.set('userLocation', userLocation);
this.set('endLocation', address);
this.send('getDistance');
},
actions: {
getDistance: function() {
// let milesAway = this.get('milesAway');
let userLocation = this.get('userLocation');
let endLocation = this.get('endLocation');
this._super(...arguments);
this.get('googleMapsApi.google').then((google) => {
var self = this;
let distanceMatrixService = new google.maps.DistanceMatrixService();
function calculateDistance() {
distanceMatrixService.getDistanceMatrix({
origins: [userLocation],
destinations: [endLocation],
travelMode: google.maps.TravelMode.DRIVING,
unitSystem: google.maps.UnitSystem.IMPERIAL,
avoidHighways: false,
avoidTolls: false
}, callback);
}
function callback(response, status) {
if (status != google.maps.DistanceMatrixStatus.OK) {
self.toggleProperty('errorState');
} else {
// var origin = response.originAddresses[0];
// var destination = response.destinationAddresses[0];
if (response.rows[0].elements[0].status === "ZERO_RESULTS") {
self.toggleProperty('errorState');
} else {
var distance = response.rows[0].elements[0].distance;
// var distance_value = distance.value;
var distance_text = distance.text;
// const miles = distance_text.substring(0, distance_text.length - 3);
self.toggleProperty('distanceLoading');
self.set('milesAway', distance_text);
later((function() {
$('.miles-away').addClass('fade-in');
}), 45);
}
}
}
calculateDistance();
});
},
}
});
components/miles-away.hbs
{{#unless distanceLoading}}
<div class="miles-away-container">
<p class="miles-away">{{milesAway}}</p>
</div>
{{/unless}}
{{yield}}
and finally, the template in which this is rendered..(just providing a snippet)
templates/vineyard/index.hbs
<div class="distance">
{{#if failState}}
{{svg-jar 'fail'}}
{{/if}}
{{#if locationIsLoading}}
{{location-loader}}
{{else}}
{{miles-away location=userLocation address=vineyard.location}}
{{/if}}
</div>
I'm open to implementing this in a completely different way, I know it's not even close to proper or perfect.

Apollo link state only works when redundant query is defined?

I have Apollo link state working:
import React from 'react';
import ReactDOM from 'react-dom';
import { HttpLink, InMemoryCache, ApolloClient } from 'apollo-client-preset';
import { WebSocketLink } from 'apollo-link-ws';
import { ApolloLink, split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { AUTH_TOKEN } from './constant';
import RootContainer from './components/RootContainer';
import { ApolloProvider } from 'react-apollo';
import { withClientState } from 'apollo-link-state';
import { gql } from 'apollo-boost';
const httpLink = new HttpLink({ uri: 'http://localhost:4000' });
const middlewareLink = new ApolloLink((operation, forward) => {
const tokenValue = localStorage.getItem(AUTH_TOKEN);
operation.setContext({
headers: {
Authorization: tokenValue ? `Bearer ${tokenValue}` : '',
},
});
return forward(operation);
});
const httpLinkAuth = middlewareLink.concat(httpLink);
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000`,
options: {
reconnect: true,
connectionParams: {
Authorization: `Bearer ${localStorage.getItem(AUTH_TOKEN)}`,
},
},
});
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLinkAuth,
);
const cache = new InMemoryCache();
const stateLink = withClientState({
cache,
defaults: {
groupMenuStatus: {
__typename: 'GroupMenuStatus',
isOpen: false,
},
},
resolvers: {
Mutation: {
updateGroupMenuStatus: (_, { isOpen }, { cache }) => {
const data = {
groupMenuStatus: {
__typename: 'GroupMenuStatus',
isOpen,
},
};
cache.writeData({ data });
return null;
},
},
Query: {
groupMenuStatus: async (_, args, { cache }) => {
const query = gql`
query groupMenuStatus {
groupMenuStatus #client {
isOpen
}
}
`;
const res = cache.readQuery({ query });
return res.groupMenuStatus;
},
},
},
});
const client = new ApolloClient({
link: ApolloLink.from([stateLink, link]),
cache,
connectToDevTools: true,
});
const token = localStorage.getItem(AUTH_TOKEN);
ReactDOM.render(
<ApolloProvider client={client}>
<RootContainer token={token} />
</ApolloProvider>,
document.getElementById('root'),
);
However is most of the examples online they havn't needed to define a query resolver. If I remove the code below then the query from the front-end will always return the default state, the mutation seems to have no effect:
Query: {
groupMenuStatus: async (_, args, { cache }) => {
const query = gql`
query groupMenuStatus {
groupMenuStatus #client {
isOpen
}
}
`;
const res = cache.readQuery({ query });
return res.groupMenuStatus;
},
},
According to the official docs on https://www.apollographql.com/docs/link/links/state.html
Query resolvers are only called on a cache miss. Since the first time you call the query will be a cache miss, you should return any default state from your resolver function.
So, if you define a default, your query resolver will never be called. (You got the definition right, it is called Query indeed)
If you do not declare a default, you might use the query resolver to write something on cache (and then the query resolver will not be called anymore), or you can just return some value, and the resolver will be called every time.
I use it, for example to get user geolocation on the first call, that's my default value now, and the resolver is never called again.
Check my use case:
Query: {
async smePosition(_: any, {}: any, { cache }: IContext): Bluebird<any> {
return new Bluebird((resolve: any, reject: any): void => {
window.navigator.geolocation.getCurrentPosition(
({coords: {latitude: lat, longitude: lng}}) => {
const data = {
smePosition: {
__typename: 'SMe',
position: {lat, lng , __typename: 'IPosition'},
},
}
cache.writeData({ data })
resolve()
},
)
})
},
},
In this case, I don't define a defaults value for 'smePosition'

single table inheritance in ember-data v1.0.0

I'm migrating an ember application from ember-data v0.13 to v1.0.0 .
In version v0.13, RESTSerializer used to have a materialize callback that allowed me to map rails STI models to ember models.
So when I get a list of events with different types, i would convert each of them to the appropriate ember model
"events": [
{
"id": 1,
"type": "cash_inflow_event",
"time": "2012-05-31T00:00:00-03:00",
"value": 30000
},
{
"id": 2,
"type": "asset_bought_event",
"asset_id": 119,
"time": "2012-08-16T00:00:00-03:00",
"quantity": 100
}
]
Ember models
App.Event = DS.Model.extend({...})
App.AssetBoughtEventMixin = Em.Mixin.create({...})
App.AssetBoughtEvent = App.Event.extend(App.AssetBoughtEventMixin)
App.CashInflowEventMixin = Em.Mixin.create({...})
App.CashInflowEvent = App.Event.extend(App.CashInflowEventMixin)
Ember-data code v0.13 that created the STI-like ember models
App.RESTSerializer = DS.RESTSerializer.extend({
materialize:function (record, serialized, prematerialized) {
var type = serialized.type;
if (type) {
var mixin = App.get(type.classify() + 'Mixin');
var klass = App.get(type.classify());
record.constructor = klass;
record.reopen(mixin);
}
this._super(record, serialized, prematerialized);
},
rootForType:function (type) {
if (type.__super__.constructor == DS.Model) {
return this._super(type);
}
else {
return this._super(type.__super__.constructor);
}
}
});
How can I do the same thing in ember-data v1.0.0 ?
I think I've got a solution...
There's a setupData callback on models.
I did the following
App.Event = DS.Model.extend({
...
setupData:function (data, partial) {
var type = data.type;
if (type) {
var mixin = App.get(type.classify() + 'Mixin');
this.reopen(mixin);
}
delete data.type;
this._super(data, partial);
},
eachAttribute: function() {
if(this.get('type')){
var constructor = App.get(this.get('type').classify());
constructor.eachAttribute.apply(constructor, arguments);
}
this._super.apply(this, arguments);
}
});
Ember experts, is this a good idea??
First, since you are using Rails, you may want to use the ActiveModelAdapter & extend your custom serializer from its serializer:
App.ApplicationAdapter = DS.ActiveModelAdapter;
App.ApplicationSerializer = DS.ActiveModelSerializer.extend({...});
It looks like your custom serializer should override typeForRoot & probably normalize. Here is what those methods looks like now:
DS.ActiveModelSerializer#typeForRoot:
typeForRoot: function(root) {
var camelized = Ember.String.camelize(root);
return Ember.String.singularize(camelized);
}
DS.JSONSerializer#normalize:
normalize: function(type, hash) {
if (!hash) { return hash; }
this.applyTransforms(type, hash);
return hash;
}