Skip to content

Commit 0266999

Browse files
committed
feat: support relations
1 parent 325b720 commit 0266999

13 files changed

+1140
-6
lines changed

src/mixins/Query.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ export default function Query(
1212
* false = include soft deletes
1313
* null = exclude soft deletes
1414
*/
15-
query.prototype.softDeletesFilter = null
15+
query.prototype.softDeleteSelectFilter = null
1616

1717
/**
1818
* Constraint includes soft deleted models.
1919
*/
2020
query.prototype.withTrashed = function() {
21-
this.softDeletesFilter = false
21+
this.softDeleteSelectFilter = false
2222

2323
return this
2424
}
@@ -27,7 +27,7 @@ export default function Query(
2727
* Constraint restricts to only soft deleted models.
2828
*/
2929
query.prototype.onlyTrashed = function() {
30-
this.softDeletesFilter = true
30+
this.softDeleteSelectFilter = true
3131

3232
return this
3333
}
@@ -98,6 +98,40 @@ export default function Query(
9898
.get()
9999
}
100100

101+
/**
102+
* Patch any new queries so that sub queries, such as relation queries,
103+
* can respect top level modifiers. For many-to-many relations, due to core
104+
* API limitations, we're forced to pass-through intermediate models.
105+
*/
106+
const newQuery = query.prototype.newQuery
107+
108+
query.prototype.newQuery = function(entity?) {
109+
const patchedQuery = newQuery.call(this, entity)
110+
111+
// Only patch queries that are loading relations.
112+
const loadables = Object.keys(this.load)
113+
114+
if (loadables.length > 0) {
115+
patchedQuery.softDeleteSelectFilter = this.softDeleteSelectFilter
116+
117+
if (entity && entity !== this.entity && this.model.hasPivotFields()) {
118+
const fields = this.model.pivotFields().reduce((fields, field) => {
119+
Object.keys(field).filter((entity) => loadables.includes(entity)).forEach((entity) => {
120+
fields.push(field[entity].pivot.entity)
121+
})
122+
return fields
123+
}, [] as string[])
124+
125+
// Release an entity that is an intermediate to a loadable relation.
126+
if (fields.includes(entity)) {
127+
patchedQuery.softDeleteSelectFilter = false
128+
}
129+
}
130+
}
131+
132+
return patchedQuery
133+
}
134+
101135
/**
102136
* Fetch all soft deletes from the store and group by entity.
103137
*/
@@ -121,12 +155,12 @@ export default function Query(
121155
) {
122156
return models.filter((model) => {
123157
// Only soft deletes
124-
if (this.softDeletesFilter === true) {
158+
if (this.softDeleteSelectFilter === true) {
125159
return model.$trashed()
126160
}
127161

128162
// Include soft deletes
129-
if (this.softDeletesFilter === false) {
163+
if (this.softDeleteSelectFilter === false) {
130164
return models
131165
}
132166

src/types/vuex-orm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ declare module '@vuex-orm/core' {
5353
/**
5454
* Filtering mode for the query builder.
5555
*/
56-
softDeletesFilter: boolean | null
56+
softDeleteSelectFilter: boolean | null
5757

5858
/**
5959
* Process the model(s) to be soft deleted.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { createStore } from 'test/support/Helpers'
2+
import { Model } from '@vuex-orm/core'
3+
4+
describe('Feature - Relations - Belongs To', () => {
5+
class User extends Model {
6+
static entity = 'users'
7+
8+
static fields() {
9+
return {
10+
id: this.attr(null)
11+
}
12+
}
13+
}
14+
15+
class Post extends Model {
16+
static entity = 'posts'
17+
18+
static fields() {
19+
return {
20+
id: this.attr(null),
21+
user_id: this.attr(null),
22+
user: this.belongsTo(User, 'user_id')
23+
}
24+
}
25+
26+
user!: User
27+
}
28+
29+
beforeEach(async () => {
30+
createStore([User, Post])
31+
32+
await Post.create({
33+
data: {
34+
id: 1,
35+
user: { id: 1 }
36+
}
37+
})
38+
})
39+
40+
it('can resolve queries without deleted relations (default)', async () => {
41+
await User.softDelete(1)
42+
43+
const post = Post.query()
44+
.with('user')
45+
.find(1) as Post
46+
47+
expect(post.user).toBeNull()
48+
})
49+
50+
it('can include deleted relations using `withTrashed` clause', async () => {
51+
await User.softDelete(1)
52+
53+
const post = Post.query()
54+
.withTrashed()
55+
.with('user')
56+
.find(1) as Post
57+
58+
expect(post.user).toBeInstanceOf(User)
59+
expect(post.user.$trashed()).toBe(true)
60+
})
61+
62+
it('can resolve only deleted relations using `onlyTrashed` clause', async () => {
63+
await User.softDelete(1)
64+
65+
const post = Post.query()
66+
.onlyTrashed()
67+
.with('user')
68+
.find(1) as Post
69+
70+
expect(post.user).toBeInstanceOf(User)
71+
expect(post.user.$trashed()).toBe(true)
72+
})
73+
})
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { createStore } from 'test/support/Helpers'
2+
import { Model } from '@vuex-orm/core'
3+
4+
describe('Feature - Relations - Belongs To Many', () => {
5+
class User extends Model {
6+
static entity = 'users'
7+
8+
static fields() {
9+
return {
10+
id: this.attr(null),
11+
roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id')
12+
}
13+
}
14+
15+
roles!: Role[]
16+
}
17+
18+
class Role extends Model {
19+
static entity = 'roles'
20+
21+
static fields() {
22+
return {
23+
id: this.attr(null)
24+
}
25+
}
26+
}
27+
28+
class RoleUser extends Model {
29+
static entity = 'roleUser'
30+
31+
static primaryKey = ['role_id', 'user_id']
32+
33+
static fields() {
34+
return {
35+
role_id: this.attr(null),
36+
user_id: this.attr(null)
37+
}
38+
}
39+
}
40+
41+
beforeEach(async () => {
42+
createStore([User, Role, RoleUser])
43+
44+
await User.create({
45+
data: [
46+
{ id: 1, roles: [{ id: 3 }, { id: 4 }] },
47+
{ id: 2, roles: [{ id: 3 }] }
48+
]
49+
})
50+
})
51+
52+
it('can resolve queries without deleted relation (default)', async () => {
53+
await Role.softDelete(3)
54+
55+
const users = User.query()
56+
.with('roles')
57+
.findIn([1, 2]) as User[]
58+
59+
expect(users[0].roles.length).toBe(1)
60+
expect(users[0].roles[0].$trashed()).toBe(false)
61+
62+
expect(users[1].roles.length).toBe(0)
63+
})
64+
65+
it('can include deleted relations using `withTrashed` clause', async () => {
66+
await Role.softDelete(3)
67+
68+
const users = User.query()
69+
.withTrashed()
70+
.with('roles')
71+
.findIn([1, 2]) as User[]
72+
73+
expect(users[0].roles.length).toBe(2)
74+
expect(users[0].roles[0].$trashed()).toBe(true)
75+
expect(users[0].roles[1].$trashed()).toBe(false)
76+
77+
expect(users[1].roles.length).toBe(1)
78+
expect(users[1].roles[0].$trashed()).toBe(true)
79+
})
80+
81+
it('can resolve only deleted relations using `onlyTrashed` clause', async () => {
82+
await Role.softDelete(3)
83+
84+
const users = User.query()
85+
.onlyTrashed()
86+
.with('roles')
87+
.findIn([1, 2]) as User[]
88+
89+
expect(users[0].roles.length).toBe(1)
90+
expect(users[0].roles[0].$trashed()).toBe(true)
91+
92+
expect(users[1].roles.length).toBe(1)
93+
expect(users[1].roles[0].$trashed()).toBe(true)
94+
})
95+
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { createStore } from 'test/support/Helpers'
2+
import { Model } from '@vuex-orm/core'
3+
4+
describe('Feature - Relations - Has Many', () => {
5+
class User extends Model {
6+
static entity = 'users'
7+
8+
static fields() {
9+
return {
10+
id: this.attr(null),
11+
posts: this.hasMany(Post, 'user_id')
12+
}
13+
}
14+
15+
posts!: Post[]
16+
}
17+
18+
class Post extends Model {
19+
static entity = 'posts'
20+
21+
static fields() {
22+
return {
23+
id: this.attr(null),
24+
user_id: this.attr(null)
25+
}
26+
}
27+
}
28+
29+
beforeEach(async () => {
30+
createStore([User, Post])
31+
32+
await User.create({
33+
data: {
34+
id: 1,
35+
posts: [{ id: 1 }, { id: 2 }]
36+
}
37+
})
38+
})
39+
40+
it('can resolve queries without deleted relations (default)', async () => {
41+
await Post.softDelete(1)
42+
43+
const user = User.query()
44+
.with('posts')
45+
.find(1) as User
46+
47+
expect(user.posts.length).toBe(1)
48+
expect(user.posts[0].$trashed()).toBe(false)
49+
})
50+
51+
it('can include deleted relations using `withTrashed` clause', async () => {
52+
await Post.softDelete(1)
53+
54+
const user = User.query()
55+
.withTrashed()
56+
.with('posts')
57+
.find(1) as User
58+
59+
expect(user.posts.length).toBe(2)
60+
expect(user.posts[0].$trashed()).toBe(true)
61+
expect(user.posts[1].$trashed()).toBe(false)
62+
})
63+
64+
it('can resolve only deleted relations using `onlyTrashed` clause', async () => {
65+
await Post.softDelete(1)
66+
67+
const user = User.query()
68+
.onlyTrashed()
69+
.with('posts')
70+
.find(1) as User
71+
72+
expect(user.posts.length).toBe(1)
73+
expect(user.posts[0].$trashed()).toBe(true)
74+
})
75+
})

0 commit comments

Comments
 (0)