Skip to content

Commit ec7f274

Browse files
committed
feat: add eager load constraints feature
1 parent 3fcbf6a commit ec7f274

File tree

5 files changed

+152
-30
lines changed

5 files changed

+152
-30
lines changed

docs/guide/relationships/getting-started.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,36 @@ Of course, you may chain more than one `with(...)` method to retrieve multiple r
9090
const user = store.$repo(User).with('posts').with('phone').first()
9191
```
9292

93+
### Loading Nested Relationships
94+
95+
To load nested relationships, you may pass constraints to the 2nd argument. For example, let's load all of the book's authors and all of the author's personal contacts:
96+
97+
```js
98+
const books = store.$repo(Book).with('author', (query) => {
99+
query.with('contacts')
100+
}).get()
101+
```
102+
103+
Learn more about the constraining query in the next section.
104+
105+
### Constraining a Relationship Query
106+
107+
Sometimes you may wish to load a relationship, but also specify additional query conditions for the loading query. Here's an example:
108+
109+
```js
110+
const users = store.$repo(User).with('posts', (query) => {
111+
query.where('published', true)
112+
}).get()
113+
```
114+
115+
In this example, it will only load posts where the post's `published` filed matches the `true` first. You may call other query builder methods to further customize the loading operation:
116+
117+
```js
118+
const users = store.$repo(User).with('posts', (query) => {
119+
query.orderBy('createdAt', 'desc')
120+
}).get()
121+
```
122+
93123
## Inserting Relationships
94124

95125
When inserting new records into the store, Vuex ORM automatically normalizes and stores data that contains any nested relationships in it's data structure. For example, let's say you have the `User` model that has a relationship to the `Post` model:

src/query/Options.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import { Model } from '../model/Model'
22
import { Query } from './Query'
33

4-
export interface Where<M extends Model, K extends keyof M> {
5-
field: WherePrimaryClosure<M> | K
6-
value: WhereSecondaryClosure<M, K> | M[K] | M[K][]
4+
export interface Where {
5+
field: WherePrimaryClosure | string
6+
value: WhereSecondaryClosure | any
77
boolean: 'and' | 'or'
88
}
99

10-
export type WherePrimaryClosure<M extends Model> = (model: M) => boolean
10+
export type WherePrimaryClosure = (model: any) => boolean
1111

12-
export type WhereSecondaryClosure<M extends Model, K extends keyof M> = (
13-
value: M[K]
14-
) => boolean
12+
export type WhereSecondaryClosure = (value: any) => boolean
1513

16-
export interface WhereGroup<M extends Model, K extends keyof M> {
17-
and?: Where<M, K>[]
18-
or?: Where<M, K>[]
14+
export interface WhereGroup {
15+
and?: Where[]
16+
or?: Where[]
1917
}
2018

2119
export interface Order<M extends Model> {

src/query/Query.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class Query<M extends Model = Model> {
6060
/**
6161
* The where constraints for the query.
6262
*/
63-
protected wheres: Where<M, any>[] = []
63+
protected wheres: Where[] = []
6464

6565
/**
6666
* The orderings for the query.
@@ -110,9 +110,9 @@ export class Query<M extends Model = Model> {
110110
/**
111111
* Add a basic where clause to the query.
112112
*/
113-
where<T extends keyof M>(
114-
field: WherePrimaryClosure<M> | T,
115-
value?: WhereSecondaryClosure<M, T> | M[T] | M[T][]
113+
where(
114+
field: WherePrimaryClosure | string,
115+
value?: WhereSecondaryClosure | any
116116
): this {
117117
this.wheres.push({ field, value, boolean: 'and' })
118118

@@ -122,7 +122,7 @@ export class Query<M extends Model = Model> {
122122
/**
123123
* Add a "where in" clause to the query.
124124
*/
125-
whereIn<T extends keyof M>(field: T, values: M[T][]): this {
125+
whereIn(field: string, values: any[]): this {
126126
this.wheres.push({ field, value: values, boolean: 'and' })
127127

128128
return this
@@ -138,9 +138,9 @@ export class Query<M extends Model = Model> {
138138
/**
139139
* Add an "or where" clause to the query.
140140
*/
141-
orWhere<T extends keyof M>(
142-
field: WherePrimaryClosure<M> | T,
143-
value?: WhereSecondaryClosure<M, T> | M[T] | M[T][]
141+
orWhere(
142+
field: WherePrimaryClosure | string,
143+
value?: WhereSecondaryClosure | any
144144
): this {
145145
this.wheres.push({ field, value, boolean: 'or' })
146146

@@ -177,8 +177,8 @@ export class Query<M extends Model = Model> {
177177
/**
178178
* Set the relationships that should be eager loaded.
179179
*/
180-
with(name: string): Query<M> {
181-
this.eagerLoad[name] = () => {}
180+
with(name: string, callback: EagerLoadConstraint = () => {}): Query<M> {
181+
this.eagerLoad[name] = callback
182182

183183
return this
184184
}
@@ -302,7 +302,7 @@ export class Query<M extends Model = Model> {
302302
/**
303303
* The function to compare where clause to the given model.
304304
*/
305-
protected whereComparator(model: M, where: Where<M, any>): boolean {
305+
protected whereComparator(model: M, where: Where): boolean {
306306
if (isFunction(where.field)) {
307307
return where.field(model)
308308
}

src/repository/Repository.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
WherePrimaryClosure,
1010
WhereSecondaryClosure,
1111
OrderDirection,
12-
OrderBy
12+
OrderBy,
13+
EagerLoadConstraint
1314
} from '../query/Options'
1415

1516
export class Repository<M extends Model = Model> {
@@ -101,19 +102,19 @@ export class Repository<M extends Model = Model> {
101102
/**
102103
* Add a basic where clause to the query.
103104
*/
104-
where<T extends keyof M>(
105-
field: WherePrimaryClosure<M> | T,
106-
value?: WhereSecondaryClosure<M, T> | M[T] | M[T][]
105+
where(
106+
field: WherePrimaryClosure | string,
107+
value?: WhereSecondaryClosure | any
107108
): Query<M> {
108109
return this.query().where(field, value)
109110
}
110111

111112
/**
112113
* Add an "or where" clause to the query.
113114
*/
114-
orWhere<T extends keyof M>(
115-
field: WherePrimaryClosure<M> | T,
116-
value?: WhereSecondaryClosure<M, T> | M[T] | M[T][]
115+
orWhere(
116+
field: WherePrimaryClosure | string,
117+
value?: WhereSecondaryClosure | any
117118
): Query<M> {
118119
return this.query().orWhere(field, value)
119120
}
@@ -142,8 +143,8 @@ export class Repository<M extends Model = Model> {
142143
/**
143144
* Set the relationships that should be eager loaded.
144145
*/
145-
with(name: string): Query<M> {
146-
return this.query().with(name)
146+
with(name: string, callback?: EagerLoadConstraint): Query<M> {
147+
return this.query().with(name, callback)
147148
}
148149

149150
/**
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { createStore, fillState } from 'test/Helpers'
2+
import { Model, Attr, Str, HasOne } from '@/index'
3+
4+
describe('feature/relations/constraints/constraints', () => {
5+
class User extends Model {
6+
static entity = 'users'
7+
8+
@Attr() id!: number
9+
@Str('') name!: string
10+
11+
@HasOne(() => Phone, 'userId')
12+
phone!: Phone | null
13+
}
14+
15+
class Phone extends Model {
16+
static entity = 'phones'
17+
18+
@Attr() id!: number
19+
@Attr() userId!: number
20+
@Str('') number!: string
21+
22+
@HasOne(() => Type, 'phoneId')
23+
type!: Type | null
24+
}
25+
26+
class Type extends Model {
27+
static entity = 'types'
28+
29+
@Attr() id!: number
30+
@Attr() phoneId!: number
31+
@Str('') name!: string
32+
}
33+
34+
it('can add constraints to the relationship query', async () => {
35+
const store = createStore()
36+
37+
fillState(store, {
38+
users: {
39+
1: { id: 1, name: 'John Doe' },
40+
2: { id: 2, name: 'Jane Doe' },
41+
3: { id: 3, name: 'Johnny Doe' }
42+
},
43+
phones: {
44+
1: { id: 1, userId: 1, number: '123' },
45+
2: { id: 2, userId: 2, number: '345' },
46+
3: { id: 3, userId: 3, number: '789' }
47+
}
48+
})
49+
50+
const users = store
51+
.$repo(User)
52+
.with('phone', (query) => {
53+
query.where('number', '345')
54+
})
55+
.get()
56+
57+
expect(users[0].phone).toBe(null)
58+
expect(users[1].phone!.number).toBe('345')
59+
expect(users[2].phone).toBe(null)
60+
})
61+
62+
it('can load nested relationships', async () => {
63+
const store = createStore()
64+
65+
fillState(store, {
66+
users: {
67+
1: { id: 1, name: 'John Doe' },
68+
2: { id: 2, name: 'Jane Doe' },
69+
3: { id: 3, name: 'Johnny Doe' }
70+
},
71+
phones: {
72+
1: { id: 1, userId: 1, number: '123' },
73+
2: { id: 2, userId: 2, number: '345' },
74+
3: { id: 3, userId: 3, number: '789' }
75+
},
76+
types: {
77+
1: { id: 1, phoneId: 1, name: 'iPhone' },
78+
2: { id: 2, phoneId: 2, name: 'Android' }
79+
}
80+
})
81+
82+
const users = store
83+
.$repo(User)
84+
.with('phone', (query) => {
85+
query.with('type')
86+
})
87+
.get()
88+
89+
expect(users[0].phone!.type!.id).toBe(1)
90+
expect(users[1].phone!.type!.id).toBe(2)
91+
expect(users[2].phone!.type).toBe(null)
92+
})
93+
})

0 commit comments

Comments
 (0)