Skip to content

Commit 2f63e3d

Browse files
author
Lee Richmond
committed
Initial commit
1 parent 41f44e4 commit 2f63e3d

36 files changed

+1339
-4
lines changed

.jshintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"predef": [
3+
"server",
34
"document",
45
"window",
56
"-Promise"

addon/mixins/model.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import Ember from 'ember';
2+
3+
const resetRelations = function(record) {
4+
record.eachRelationship((relationName, meta) => {
5+
if (meta.kind === 'hasMany') {
6+
record.set(relationName, Ember.A());
7+
} else {
8+
record.set(relationName, null);
9+
}
10+
});
11+
};
12+
13+
const defaultOptions = function(options) {
14+
if (options.resetRelations !== false) {
15+
options.resetRelations = true;
16+
}
17+
};
18+
19+
export default Ember.Mixin.create({
20+
hasDirtyAttributes: Ember.computed('currentState.isDirty', 'markedForDestruction', 'markedForDeletion', function() {
21+
let original = this._super(...arguments);
22+
return original || this.get('markedForDestruction') || this.get('markedForDeletion');
23+
}),
24+
25+
markedForDeletion: Ember.computed('_markedForDeletion', function() {
26+
return this.get('_markedForDeletion') || false;
27+
}),
28+
29+
markedForDestruction: Ember.computed('_markedForDestruction', function() {
30+
return this.get('_markedForDestruction') || false;
31+
}),
32+
33+
markForDeletion() {
34+
this.set('_markedForDeletion', true);
35+
},
36+
37+
markForDestruction() {
38+
this.set('_markedForDestruction', true);
39+
},
40+
41+
// Blank out all relations after saving
42+
// We will use the server response includes to 'reset'
43+
// these relations
44+
save(options = {}) {
45+
defaultOptions(options);
46+
let promise = this._super(...arguments);
47+
48+
if (options.resetRelations) {
49+
promise.then(resetRelations);
50+
}
51+
52+
return promise;
53+
}
54+
});

addon/mixins/nested-relations.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import Ember from 'ember';
2+
3+
const iterateRelations = function(record, relations, callback) {
4+
Object.keys(relations).forEach((relationName) => {
5+
let subRelations = relations[relationName];
6+
7+
let kind = record.relationshipFor(relationName).kind;
8+
let relatedRecord = record.get(relationName);
9+
relatedRecord = relatedRecord.get('content');
10+
11+
if (relatedRecord) {
12+
callback(relationName, kind, relatedRecord, subRelations);
13+
}
14+
});
15+
};
16+
17+
const isPresentObject = function(val) {
18+
return val && Object.keys(val).length > 0;
19+
};
20+
21+
const attributesFor = function(record) {
22+
let attrs = {};
23+
24+
let changes = record.changedAttributes();
25+
record.eachAttribute((name/* meta */) => {
26+
if (record.get('isNew') || changes[name]) {
27+
let value = record.get(name);
28+
29+
if (value !== undefined) {
30+
attrs[name] = record.get(name);
31+
}
32+
}
33+
});
34+
35+
if (record.get('markedForDeletion')) {
36+
attrs = { _delete: true };
37+
}
38+
39+
if (record.get('markedForDestruction')) {
40+
attrs = { _destroy: true };
41+
}
42+
43+
return attrs;
44+
};
45+
46+
const jsonapiType = function(record) {
47+
return record.store
48+
.adapterFor(record.constructor.modelName)
49+
.pathForType(record.constructor.modelName);
50+
};
51+
52+
const jsonapiPayload = function(record) {
53+
let attributes = attributesFor(record);
54+
55+
let payload = { type: jsonapiType(record) };
56+
57+
if (isPresentObject(attributes)) {
58+
payload.attributes = attributes;
59+
}
60+
61+
if (record.id) {
62+
payload.id = record.id;
63+
}
64+
65+
return payload;
66+
};
67+
68+
const hasManyData = function(relatedRecords, subRelations) {
69+
let payloads = [];
70+
relatedRecords.forEach((relatedRecord) => {
71+
if (relatedRecord.get('hasDirtyAttributes')) {
72+
let payload = jsonapiPayload(relatedRecord);
73+
processRelationships(subRelations, payload, relatedRecord);
74+
payloads.push(payload);
75+
}
76+
});
77+
return { data: payloads };
78+
};
79+
80+
const belongsToData = function(relatedRecord, subRelations) {
81+
if (isPresentObject(subRelations) || relatedRecord.get('hasDirtyAttributes')) {
82+
let payload = jsonapiPayload(relatedRecord);
83+
processRelationships(subRelations, payload, relatedRecord);
84+
return { data: payload };
85+
}
86+
};
87+
88+
const processRelationship = function(kind, relationData, subRelations, callback) {
89+
let payload = null;
90+
91+
if (kind === 'hasMany') {
92+
payload = hasManyData(relationData, subRelations);
93+
} else {
94+
payload = belongsToData(relationData, subRelations);
95+
}
96+
97+
if (payload && payload.data) {
98+
callback(payload);
99+
}
100+
};
101+
102+
const processRelationships = function(relationshipHash, jsonData, record) {
103+
if (isPresentObject(relationshipHash)) {
104+
jsonData.relationships = {};
105+
106+
iterateRelations(record, relationshipHash, (name, kind, related, subRelations) => {
107+
processRelationship(kind, related, subRelations, (payload) => {
108+
jsonData.relationships[name] = payload;
109+
});
110+
});
111+
}
112+
};
113+
114+
const relationshipsDirective = function(value) {
115+
let directive = {};
116+
117+
if (value) {
118+
if (typeof(value) === 'string') {
119+
directive[value] = {};
120+
} else if(Array.isArray(value)) {
121+
value.forEach((key) => {
122+
Ember.merge(directive, relationshipsDirective(key));
123+
});
124+
} else {
125+
Object.keys(value).forEach((key) => {
126+
directive[key] = relationshipsDirective(value[key]);
127+
});
128+
}
129+
} else {
130+
return {};
131+
}
132+
133+
return directive;
134+
};
135+
136+
export default Ember.Mixin.create({
137+
serialize(snapshot/*, options */) {
138+
let json = this._super(...arguments);
139+
delete(json.data.relationships);
140+
delete(json.data.attributes);
141+
142+
let adapterOptions = snapshot.adapterOptions || {};
143+
144+
let attributes = attributesFor(snapshot.record);
145+
if (isPresentObject(attributes)) {
146+
json.data.attributes = attributes;
147+
}
148+
149+
if (snapshot.record.id) {
150+
json.data.id = snapshot.record.id.toString();
151+
}
152+
153+
if (adapterOptions.attributes === false) {
154+
delete(json.data.attributes);
155+
}
156+
157+
let relationships = relationshipsDirective(adapterOptions.relationships);
158+
processRelationships(relationships, json.data, snapshot.record);
159+
console.log('serialized', json);
160+
return json;
161+
}
162+
});

bower.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"dependencies": {
44
"ember": "~2.7.0",
55
"ember-cli-shims": "0.1.1",
6-
"ember-qunit-notifications": "0.1.0"
6+
"ember-qunit-notifications": "0.1.0",
7+
"pretender": "~1.1.0",
8+
"Faker": "~3.1.0",
9+
"materialize": "0.97.0"
710
}
811
}

ember-cli-build.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ module.exports = function(defaults) {
77
// Add options here
88
});
99

10+
app.import('bower_components/materialize/dist/css/materialize.css', { prepend: true });
11+
1012
/*
1113
This build file specifies the options for the dummy test app of this
1214
addon, located in `/tests/dummy`

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,24 @@
2727
"ember-cli-htmlbars-inline-precompile": "^0.3.1",
2828
"ember-cli-inject-live-reload": "^1.4.0",
2929
"ember-cli-jshint": "^1.0.0",
30+
"ember-cli-mirage": "0.2.1",
31+
"ember-cli-page-object": "1.6.0",
3032
"ember-cli-qunit": "^2.0.0",
3133
"ember-cli-release": "^0.2.9",
3234
"ember-cli-sri": "^2.1.0",
3335
"ember-cli-test-loader": "^1.1.0",
3436
"ember-cli-uglify": "^1.2.0",
35-
"ember-data": "^2.7.0",
3637
"ember-disable-prototype-extensions": "^1.1.0",
3738
"ember-export-application-global": "^1.0.5",
3839
"ember-load-initializers": "^0.5.1",
3940
"ember-resolver": "^2.0.3",
40-
"ember-welcome-page": "^1.0.1",
4141
"loader.js": "^4.0.1"
4242
},
4343
"keywords": [
4444
"ember-addon"
4545
],
4646
"dependencies": {
47+
"ember-data": "^2.7.0",
4748
"ember-cli-babel": "^5.1.6"
4849
},
4950
"ember-addon": {

tests/.jshintrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"predef": [
3+
"server",
34
"document",
45
"window",
56
"location",
@@ -21,7 +22,9 @@
2122
"andThen",
2223
"currentURL",
2324
"currentPath",
24-
"currentRouteName"
25+
"currentRouteName",
26+
"test",
27+
"QUnit"
2528
],
2629
"node": false,
2730
"browser": false,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { test } from 'qunit';
2+
import moduleForAcceptance from '../../tests/helpers/module-for-acceptance';
3+
import page from 'dummy/tests/pages/post-form';
4+
import detailPage from 'dummy/tests/pages/show-post';
5+
6+
moduleForAcceptance('Acceptance | create nested relations');
7+
8+
test('creating a record with nested relations', function(assert) {
9+
page
10+
.visit()
11+
.title.fillIn('my post');
12+
13+
page.addTag().tags(0).setName('new tag 1');
14+
page.addTag().tags(1).setName('new tag 2');
15+
page.addTag();
16+
page.authorName.fillIn('John Doe');
17+
page.submit();
18+
19+
andThen(function() {
20+
assert.equal(detailPage.title, 'my post', 'saves basic attributes correctly');
21+
assert.equal(detailPage.tagList, 'new tag 1, new tag 2', 'saves one-to-many correctly');
22+
assert.equal(detailPage.authorName, 'John Doe', 'saves one-to-one correctly');
23+
});
24+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test } from 'qunit';
2+
import moduleForAcceptance from '../../tests/helpers/module-for-acceptance';
3+
import page from 'dummy/tests/pages/post-form';
4+
import detailPage from 'dummy/tests/pages/show-post';
5+
6+
moduleForAcceptance('Acceptance | delete nested objects');
7+
8+
test('deleting a nested object', function(assert) {
9+
page.visit();
10+
page.addTag().tags(0).setName('new tag 1');
11+
page.submit();
12+
13+
andThen(function() {
14+
assert.equal(detailPage.tagList, 'new tag 1');
15+
detailPage.edit();
16+
17+
andThen(function() {
18+
page.tags(0).remove();
19+
20+
andThen(function() {
21+
assert.equal(page.tags().count, 0, 'should not show removed tag');
22+
page.submit();
23+
24+
andThen(function() {
25+
assert.equal(detailPage.tagList, '', 'should delete removed tag');
26+
});
27+
});
28+
});
29+
});
30+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { test } from 'qunit';
2+
import moduleForAcceptance from '../../tests/helpers/module-for-acceptance';
3+
import page from 'dummy/tests/pages/post-form';
4+
import detailPage from 'dummy/tests/pages/show-post';
5+
6+
moduleForAcceptance('Acceptance | update nested relations');
7+
8+
// Todo test ideally
9+
// * delete
10+
// * disassociate
11+
test('updating nested relations', function(assert) {
12+
let author = server.create('author', { name: 'Joe Author' });
13+
let tag1 = server.create('tag', { name: 'tag1' });
14+
let tag2 = server.create('tag', { name: 'tag2' });
15+
let post = server.create('post', {
16+
author: author,
17+
tags: [tag1, tag2],
18+
title: 'test title'
19+
});
20+
visit(`/posts/${post.id}/edit`);
21+
22+
andThen(function() {
23+
assert.equal(page.title.val, 'test title');
24+
assert.equal(page.authorName.val, 'Joe Author');
25+
assert.equal(page.tags().count, 2);
26+
assert.equal(page.tags(0).name, 'tag1');
27+
assert.equal(page.tags(1).name, 'tag2');
28+
29+
page.authorName.fillIn('new author');
30+
page.tags(1).setName('tag2 changed');
31+
page.addTag();
32+
page.tags(2).setName('new tag');
33+
page.submit();
34+
35+
andThen(function() {
36+
post.reload();
37+
assert.equal(post.author.id, author.id, 'updates existing author');
38+
assert.equal(detailPage.authorName, 'new author', 'updates author name');
39+
assert.equal(detailPage.tagList, 'tag1, tag2 changed, new tag', 'updates one-to-many correctly');
40+
});
41+
});
42+
});

0 commit comments

Comments
 (0)