Skip to content

Commit a7a2647

Browse files
committed
Experimental support for polymorphic relations
1 parent 34568a9 commit a7a2647

File tree

9 files changed

+564
-496
lines changed

9 files changed

+564
-496
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ the server. It's true for all records except newly created ones.
135135

136136
## Known issues
137137

138-
- n:m relations and polymorphic relations are untested
139138
- Each model is only loaded once in the graph
140139
- Code documentation is deprecated and crappy
141140

dist/vuex-orm-apollo.esm.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7842,21 +7842,25 @@ var QueryBuilder = /** @class */ (function () {
78427842
Object.keys(data).forEach(function (key) {
78437843
if (data[key] !== undefined && data[key] !== null) {
78447844
if (data[key] instanceof Object) {
7845+
var localModel = _this.context.getModel(key, true) || model;
78457846
if (data[key].nodes) {
7846-
result[inflection.pluralize(key)] = _this.transformIncomingData(data[key].nodes, model, mutation, true);
7847+
result[inflection.pluralize(key)] = _this.transformIncomingData(data[key].nodes, localModel, mutation, true);
78477848
}
78487849
else {
78497850
var newKey = key;
78507851
if (mutation && !recursiveCall) {
7851-
newKey = data[key].nodes ? model.pluralName : model.singularName;
7852+
newKey = data[key].nodes ? localModel.pluralName : localModel.singularName;
78527853
newKey = downcaseFirstLetter(newKey);
78537854
}
7854-
result[newKey] = _this.transformIncomingData(data[key], model, mutation, true);
7855+
result[newKey] = _this.transformIncomingData(data[key], localModel, mutation, true);
78557856
}
78567857
}
7857-
else if (key === 'id') {
7858+
else if (model.fieldIsNumber(model.fields.get(key))) {
78587859
result[key] = parseInt(data[key], 0);
78597860
}
7861+
else if (key.endsWith('Type') && model.isTypeFieldOfPolymorphRelation(key)) {
7862+
result[key] = inflection.pluralize(downcaseFirstLetter(data[key]));
7863+
}
78607864
else {
78617865
result[key] = data[key];
78627866
}
@@ -8164,6 +8168,41 @@ var Model = /** @class */ (function () {
81648168
});
81658169
return relations;
81668170
};
8171+
/**
8172+
* This accepts a field like `subjectType` and checks if this just randomly is called `...Type` or it is part
8173+
* of a polymorph relation.
8174+
* @param {string} name
8175+
* @returns {boolean}
8176+
*/
8177+
Model.prototype.isTypeFieldOfPolymorphRelation = function (name) {
8178+
var _this = this;
8179+
var found = false;
8180+
this.context.models.forEach(function (model) {
8181+
if (found)
8182+
return false;
8183+
model.getRelations().forEach(function (relation) {
8184+
if (relation instanceof _this.context.components.MorphMany ||
8185+
relation instanceof _this.context.components.MorphedByMany ||
8186+
relation instanceof _this.context.components.MorphOne ||
8187+
relation instanceof _this.context.components.MorphTo ||
8188+
relation instanceof _this.context.components.MorphToMany) {
8189+
if (relation.type === name && relation.related && relation.related.entity === _this.baseModel.entity) {
8190+
found = true;
8191+
return false;
8192+
}
8193+
}
8194+
return true;
8195+
});
8196+
return true;
8197+
});
8198+
return found;
8199+
};
8200+
Model.prototype.fieldIsNumber = function (field) {
8201+
if (!field)
8202+
return false;
8203+
return field instanceof this.context.components.Number ||
8204+
field instanceof this.context.components.Increment;
8205+
};
81678206
Model.prototype.fieldIsAttribute = function (field) {
81688207
return field instanceof this.context.components.Increment ||
81698208
field instanceof this.context.components.Attr ||
@@ -8199,13 +8238,15 @@ var Context = /** @class */ (function () {
81998238
* Returns a model by name
82008239
*
82018240
* @param {Model|string} model
8241+
* @param allowNull
82028242
* @returns {Model}
82038243
*/
8204-
Context.prototype.getModel = function (model) {
8244+
Context.prototype.getModel = function (model, allowNull) {
8245+
if (allowNull === void 0) { allowNull = false; }
82058246
if (typeof model === 'string') {
82068247
var name_1 = inflection$2.singularize(downcaseFirstLetter(model));
82078248
model = this.models.get(name_1);
8208-
if (!model)
8249+
if (!allowNull && !model)
82098250
throw new Error("No such model " + name_1 + "!");
82108251
}
82118252
return model;

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@
8181
"tslint-config-standard": "^7.0.0",
8282
"typescript": "^2.7.1",
8383
"uglify-js": "^3.3.9",
84+
"vuex": "^3.0.1",
8485
"webpack": "^3.10.0",
85-
"webpack-node-externals": "^1.6.0",
86-
"vuex": "^3.0.1"
86+
"webpack-node-externals": "^1.6.0"
8787
},
8888
"nyc": {
8989
"include": [

src/context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ export default class Context {
3939
* Returns a model by name
4040
*
4141
* @param {Model|string} model
42+
* @param allowNull
4243
* @returns {Model}
4344
*/
44-
public getModel (model: Model | string): Model {
45+
public getModel (model: Model | string, allowNull: boolean = false): Model {
4546
if (typeof model === 'string') {
4647
const name: string = inflection.singularize(downcaseFirstLetter(model));
4748
model = this.models.get(name) as Model;
48-
if (!model) throw new Error(`No such model ${name}!`);
49+
if (!allowNull && !model) throw new Error(`No such model ${name}!`);
4950
}
5051

5152
return model;

src/model.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,46 @@ export default class Model {
8080
return relations;
8181
}
8282

83+
/**
84+
* This accepts a field like `subjectType` and checks if this just randomly is called `...Type` or it is part
85+
* of a polymorph relation.
86+
* @param {string} name
87+
* @returns {boolean}
88+
*/
89+
public isTypeFieldOfPolymorphRelation (name: string): boolean {
90+
let found: boolean = false;
91+
92+
this.context.models.forEach((model) => {
93+
if (found) return false;
94+
95+
model.getRelations().forEach((relation) => {
96+
if (relation instanceof this.context.components.MorphMany ||
97+
relation instanceof this.context.components.MorphedByMany ||
98+
relation instanceof this.context.components.MorphOne ||
99+
relation instanceof this.context.components.MorphTo ||
100+
relation instanceof this.context.components.MorphToMany) {
101+
102+
if (relation.type === name && relation.related && relation.related.entity === this.baseModel.entity) {
103+
found = true;
104+
return false;
105+
}
106+
}
107+
108+
return true;
109+
});
110+
111+
return true;
112+
});
113+
114+
return found;
115+
}
116+
117+
public fieldIsNumber (field: Field | undefined): boolean {
118+
if (!field) return false;
119+
return field instanceof this.context.components.Number ||
120+
field instanceof this.context.components.Increment;
121+
}
122+
83123
private fieldIsAttribute (field: Field): boolean {
84124
return field instanceof this.context.components.Increment ||
85125
field instanceof this.context.components.Attr ||

src/queryBuilder.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { print } from 'graphql/language/printer';
55
import { Arguments, Data, Field } from './interfaces';
66
import { downcaseFirstLetter, upcaseFirstLetter } from './utils';
77
import gql from 'graphql-tag';
8-
import { BelongsTo } from '@vuex-orm/core';
98
import Context from './context';
109

1110
const inflection = require('inflection');
@@ -171,20 +170,25 @@ export default class QueryBuilder {
171170
Object.keys(data).forEach((key) => {
172171
if (data[key] !== undefined && data[key] !== null) {
173172
if (data[key] instanceof Object) {
173+
const localModel: Model = this.context.getModel(key, true) || model;
174+
174175
if (data[key].nodes) {
175-
result[inflection.pluralize(key)] = this.transformIncomingData(data[key].nodes, model, mutation, true);
176+
result[inflection.pluralize(key)] = this.transformIncomingData(data[key].nodes,
177+
localModel, mutation, true);
176178
} else {
177179
let newKey = key;
178180

179181
if (mutation && !recursiveCall) {
180-
newKey = data[key].nodes ? model.pluralName : model.singularName;
182+
newKey = data[key].nodes ? localModel.pluralName : localModel.singularName;
181183
newKey = downcaseFirstLetter(newKey);
182184
}
183185

184-
result[newKey] = this.transformIncomingData(data[key], model, mutation, true);
186+
result[newKey] = this.transformIncomingData(data[key], localModel, mutation, true);
185187
}
186-
} else if (key === 'id') {
188+
} else if (model.fieldIsNumber(model.fields.get(key))) {
187189
result[key] = parseInt(data[key], 0);
190+
} else if (key.endsWith('Type') && model.isTypeFieldOfPolymorphRelation(key)) {
191+
result[key] = inflection.pluralize(downcaseFirstLetter(data[key]));
188192
} else {
189193
result[key] = data[key];
190194
}

test/integration/VuexORMApollo.spec.js

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import {Model as ORMModel} from "@vuex-orm/core";
2-
import Vue from 'vue';
32
import {createStore, sendWithMockFetch} from "../support/Helpers";
4-
import fetchMock from 'fetch-mock';
53

64
let store;
75
let vuexOrmApollo;
@@ -19,6 +17,23 @@ class User extends ORMModel {
1917
}
2018
}
2119

20+
class Video extends ORMModel {
21+
static entity = 'videos';
22+
static eagerLoad = ['comments'];
23+
24+
static fields () {
25+
return {
26+
id: this.increment(null),
27+
content: this.string(''),
28+
title: this.string(''),
29+
userId: this.number(0),
30+
otherId: this.number(0), // This is a field which ends with `Id` but doesn't belong to any relation
31+
user: this.belongsTo(User, 'userId'),
32+
comments: this.morphMany(Comment, 'subjectId', 'subjectType')
33+
};
34+
}
35+
}
36+
2237
class Post extends ORMModel {
2338
static entity = 'posts';
2439
static eagerLoad = ['comments'];
@@ -31,7 +46,7 @@ class Post extends ORMModel {
3146
userId: this.number(0),
3247
otherId: this.number(0), // This is a field which ends with `Id` but doesn't belong to any relation
3348
user: this.belongsTo(User, 'userId'),
34-
comments: this.hasMany(Comment, 'userId')
49+
comments: this.morphMany(Comment, 'subjectId', 'subjectType')
3550
};
3651
}
3752
}
@@ -45,23 +60,26 @@ class Comment extends ORMModel {
4560
id: this.increment(0),
4661
content: this.string(''),
4762
userId: this.number(0),
48-
postId: this.number(0),
4963
user: this.belongsTo(User, 'userId'),
50-
post: this.belongsTo(Post, 'postId')
64+
65+
subjectId: this.number(0),
66+
subjectType: this.string('')
5167
};
5268
}
5369
}
5470

5571
describe('VuexORMApollo', () => {
5672
beforeEach(() => {
57-
[store, vuexOrmApollo] = createStore([{ model: User }, { model: Post }, { model: Comment }]);
73+
[store, vuexOrmApollo] = createStore([{ model: User }, { model: Post }, { model: Video }, { model: Comment }]);
5874

5975
store.dispatch('entities/users/insert', { data: { id: 1, name: 'Charlie Brown' }});
6076
store.dispatch('entities/users/insert', { data: { id: 2, name: 'Peppermint Patty' }});
6177
store.dispatch('entities/posts/insert', { data: { id: 1, userId: 1, title: 'Example post 1', content: 'Foo' }});
6278
store.dispatch('entities/posts/insert', { data: { id: 1, userId: 1, title: 'Example post 2', content: 'Bar' }});
63-
store.dispatch('entities/comments/insert', { data: { id: 1, userId: 1, postId: 1, content: 'Example comment 1' }});
64-
store.dispatch('entities/comments/insert', { data: { id: 1, userId: 2, postId: 1, content: 'Example comment 2' }});
79+
store.dispatch('entities/videos/insert', { data: { id: 1, userId: 1, title: 'Example video', content: 'Video' }});
80+
store.dispatch('entities/comments/insert', { data: { id: 1, userId: 1, subjectId: 1, subjectType: 'videos', content: 'Example comment 1' }});
81+
store.dispatch('entities/comments/insert', { data: { id: 1, userId: 2, subjectId: 1, subjectType: 'posts', content: 'Example comment 2' }});
82+
store.dispatch('entities/comments/insert', { data: { id: 1, userId: 2, subjectId: 2, subjectType: 'posts', content: 'Example comment 3' }});
6583
});
6684

6785
describe('fetch', () => {
@@ -70,13 +88,19 @@ describe('VuexORMApollo', () => {
7088
data: {
7189
post: {
7290
__typename: 'post',
73-
id: 1,
91+
id: 42,
7492
otherId: 13548,
75-
title: 'Example Post 1',
93+
title: 'Example Post 5',
7694
content: 'Foo',
7795
comments: {
7896
__typename: 'comment',
79-
nodes: []
97+
nodes: [{
98+
__typename: 'comment',
99+
id: 15,
100+
content: 'Works!',
101+
subjectId: 42,
102+
subjectType: 'Post'
103+
}]
80104
},
81105
user: {
82106
__typename: 'user',
@@ -88,7 +112,7 @@ describe('VuexORMApollo', () => {
88112
};
89113

90114
let request = await sendWithMockFetch(response, async () => {
91-
await store.dispatch('entities/posts/fetch', { filter: { id: 1 } });
115+
await store.dispatch('entities/posts/fetch', { filter: { id: 42 } });
92116
});
93117
expect(request).not.toEqual(null);
94118

@@ -108,6 +132,8 @@ query Post($id: ID!) {
108132
nodes {
109133
id
110134
content
135+
subjectId
136+
subjectType
111137
__typename
112138
}
113139
__typename
@@ -116,6 +142,11 @@ query Post($id: ID!) {
116142
}
117143
}
118144
`.trim() + "\n");
145+
146+
const post = store.getters['entities/posts/query']().withAll().where('id', 42).first();
147+
expect(post.title).toEqual('Example Post 5');
148+
expect(post.comments.length).toEqual(1);
149+
expect(post.comments[0].content).toEqual('Works!');
119150
});
120151

121152

@@ -392,6 +423,8 @@ mutation UpvotePost($post: PostInput!, $captchaToken: String!) {
392423
nodes {
393424
id
394425
content
426+
subjectId
427+
subjectType
395428
__typename
396429
}
397430
__typename

0 commit comments

Comments
 (0)