Skip to content

Commit 6c8cce5

Browse files
authored
feat: add has many by relation (#35) (#37)
close #35
1 parent bed9e72 commit 6c8cce5

File tree

11 files changed

+642
-3
lines changed

11 files changed

+642
-3
lines changed

docs/guide/model/decorators.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class User extends Model {
162162
Marks a property on the model as a [belongsTo attribute](../relationships/getting-started.md) type. For example:
163163

164164
```ts
165-
import { Model, BelongsTo } from '@vuex-orm/core'
165+
import { Model, Attr, BelongsTo } from '@vuex-orm/core'
166166
import User from '@/models/User'
167167

168168
class Post extends Model {
@@ -172,7 +172,7 @@ class Post extends Model {
172172
userId!: number | null
173173

174174
@BelongsTo(() => User, 'userId')
175-
user: User | null
175+
user!: User | null
176176
}
177177
```
178178

@@ -188,6 +188,25 @@ class User extends Model {
188188
static entity = 'users'
189189

190190
@HasMany(() => Post, 'userId')
191-
posts: Post[]
191+
posts!: Post[]
192+
}
193+
```
194+
195+
### `@HasManyBy`
196+
197+
Marks a property on the model as a [hasManyBy attribute](../relationships/one-to-many) type. For example:
198+
199+
```ts
200+
import { Model, HasManyBy } from '@vuex-orm/core'
201+
import Post from '@/models/Post'
202+
203+
class Cluster extends Model {
204+
static entity = 'clusters'
205+
206+
@Attr(null)
207+
nodeIds!: number[]
208+
209+
@HasManyBy(() => Node, 'nodesId')
210+
nodes!: Node[]
192211
}
193212
```

docs/guide/relationships/one-to-many.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,69 @@ class Comment extends Model {
8888
}
8989
}
9090
```
91+
92+
## One To Many By
93+
94+
One To Many By is similar to One To Many relation but having foreign keys at parent Model as an array. For example, there could be a situation where you must parse data looks something like:
95+
96+
```js
97+
{
98+
nodes: {
99+
1: { id: 1, name: 'Node 01' },
100+
2: { id: 2, name: 'Node 02' }
101+
},
102+
clusters: {
103+
1: {
104+
id: 1,
105+
name: 'Cluster 01',
106+
nodeIds: [1, 2]
107+
}
108+
}
109+
}
110+
```
111+
112+
As you can see, `clusters` have "Has Many" relationship with `nodes`, but `nodes` do not have foreign key set (`clusterId`). We can't use Has Many relation in this case because there is no foreign key to look for. In such cases, you may use `hasManyBy` relationship.
113+
114+
```js
115+
class Node extends Model {
116+
static entity = 'nodes'
117+
118+
static fields () {
119+
return {
120+
id: this.attr(null),
121+
name: this.string('')
122+
}
123+
}
124+
}
125+
126+
class Cluster extends Model {
127+
static entity = 'clusters'
128+
129+
static fields () {
130+
return {
131+
id: this.attr(null),
132+
nodeIds: this.attr([]),
133+
name: this.string('')
134+
nodes: this.hasManyBy(Node, 'nodeIds')
135+
}
136+
}
137+
}
138+
```
139+
140+
Now the Cluster model is going to look for Nodes using ids at Cluster's own `nodeIds` attributes.
141+
142+
As always, you can pass the third argument to specify which id to look for.
143+
144+
```js
145+
class Cluster extends Model {
146+
static entity = 'clusters'
147+
148+
static fields () {
149+
return {
150+
id: this.attr(null),
151+
nodeIds: this.attr(null),
152+
nodes: this.hasManyBy(Node, 'nodeIds', 'nodeId')
153+
}
154+
}
155+
}
156+
```

src/index.cjs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Uid as UidAttr } from './model/attributes/types/Uid'
2424
import { Relation } from './model/attributes/relations/Relation'
2525
import { HasOne as HasOneAttr } from './model/attributes/relations/HasOne'
2626
import { HasMany as HasManyAttr } from './model/attributes/relations/HasMany'
27+
import { HasManyBy as HasManyByAttr } from './model/attributes/relations/HasManyBy'
2728
import { Repository } from './repository/Repository'
2829
import { Interpreter } from './interpreter/Interpreter'
2930
import { Query } from './query/Query'
@@ -54,6 +55,7 @@ export default {
5455
Relation,
5556
HasOneAttr,
5657
HasManyAttr,
58+
HasManyByAttr,
5759
Repository,
5860
Interpreter,
5961
Query,

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from './model/decorators/attributes/types/Uid'
1414
export * from './model/decorators/attributes/relations/HasOne'
1515
export * from './model/decorators/attributes/relations/BelongsTo'
1616
export * from './model/decorators/attributes/relations/HasMany'
17+
export * from './model/decorators/attributes/relations/HasManyBy'
1718
export * from './model/decorators/Contracts'
1819
export * from './model/decorators/NonEnumerable'
1920
export * from './model/attributes/Attribute'
@@ -27,6 +28,7 @@ export * from './model/attributes/relations/Relation'
2728
export { HasOne as HasOneAttr } from './model/attributes/relations/HasOne'
2829
export { BelongsTo as BelongsToAttr } from './model/attributes/relations/BelongsTo'
2930
export { HasMany as HasManyAttr } from './model/attributes/relations/HasMany'
31+
export { HasManyBy as HasManyByAttr } from './model/attributes/relations/HasManyBy'
3032
export * from './modules/RootModule'
3133
export * from './modules/RootState'
3234
export * from './modules/Module'
@@ -56,6 +58,7 @@ import { Uid as UidAttr } from './model/attributes/types/Uid'
5658
import { Relation } from './model/attributes/relations/Relation'
5759
import { HasOne as HasOneAttr } from './model/attributes/relations/HasOne'
5860
import { HasMany as HasManyAttr } from './model/attributes/relations/HasMany'
61+
import { HasManyBy as HasManyByAttr } from './model/attributes/relations/HasManyBy'
5962
import { Repository } from './repository/Repository'
6063
import { Interpreter } from './interpreter/Interpreter'
6164
import { Query } from './query/Query'
@@ -78,6 +81,7 @@ export default {
7881
Relation,
7982
HasOneAttr,
8083
HasManyAttr,
84+
HasManyByAttr,
8185
Repository,
8286
Interpreter,
8387
Query,

src/model/Model.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Relation } from './attributes/relations/Relation'
1313
import { HasOne } from './attributes/relations/HasOne'
1414
import { BelongsTo } from './attributes/relations/BelongsTo'
1515
import { HasMany } from './attributes/relations/HasMany'
16+
import { HasManyBy } from './attributes/relations/HasManyBy'
1617

1718
export type ModelFields = Record<string, Attribute>
1819
export type ModelSchemas = Record<string, ModelFields>
@@ -219,6 +220,26 @@ export class Model {
219220
return new HasMany(model, related.newRawInstance(), foreignKey, localKey)
220221
}
221222

223+
/**
224+
* Create a new HasManyBy relation instance.
225+
*/
226+
static hasManyBy(
227+
related: typeof Model,
228+
foreignKey: string,
229+
ownerKey?: string
230+
): HasManyBy {
231+
const instance = related.newRawInstance()
232+
233+
ownerKey = ownerKey ?? instance.$getLocalKey()
234+
235+
return new HasManyBy(
236+
instance,
237+
related.newRawInstance(),
238+
foreignKey,
239+
ownerKey
240+
)
241+
}
242+
222243
/**
223244
* Get the constructor for this model.
224245
*/
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { Schema as NormalizrSchema } from 'normalizr'
2+
import { Schema } from '../../../schema/Schema'
3+
import { Element, Collection } from '../../../data/Data'
4+
import { Query } from '../../../query/Query'
5+
import { Model } from '../../Model'
6+
import { Relation } from './Relation'
7+
8+
export class HasManyBy extends Relation {
9+
/**
10+
* The child model instance of the relation.
11+
*/
12+
protected child: Model
13+
14+
/**
15+
* The foreign key of the parent model.
16+
*/
17+
protected foreignKey: string
18+
19+
/**
20+
* The owner key of the parent model.
21+
*/
22+
protected ownerKey: string
23+
24+
/**
25+
* Create a new has-many-by relation instance.
26+
*/
27+
constructor(
28+
parent: Model,
29+
child: Model,
30+
foreignKey: string,
31+
ownerKey: string
32+
) {
33+
super(parent, child)
34+
this.foreignKey = foreignKey
35+
this.ownerKey = ownerKey
36+
37+
// In the underlying base relation class, this property is referred to as
38+
// the "parent" as most relations are not inversed. But, since this
39+
// one is, we will create a "child" property for improved readability.
40+
this.child = child
41+
}
42+
43+
/**
44+
* Get all related models for the relationship.
45+
*/
46+
getRelateds(): Model[] {
47+
return [this.child]
48+
}
49+
50+
/**
51+
* Define the normalizr schema for the relation.
52+
*/
53+
define(schema: Schema): NormalizrSchema {
54+
return schema.many(this.child)
55+
}
56+
57+
/**
58+
* Attach the relational key to the given relation.
59+
*/
60+
attach(record: Element, child: Element): void {
61+
// If the child doesn't contain the owner key, just skip here. This happens
62+
// when child items have uid attribute as its primary key, and it's missing
63+
// when inserting records. Those ids will be generated later and will be
64+
// looped again. At that time, we can attach the correct owner key value.
65+
if (child[this.ownerKey] === undefined) {
66+
return
67+
}
68+
69+
if (!record[this.foreignKey]) {
70+
record[this.foreignKey] = []
71+
}
72+
73+
this.attachIfMissing(record[this.foreignKey], child[this.ownerKey])
74+
}
75+
76+
/**
77+
* Push owner key to foregin key array if owner key doesn't exist in foreign
78+
* key array.
79+
*/
80+
protected attachIfMissing(
81+
foreignKey: (string | number)[],
82+
ownerKey: string | number
83+
): void {
84+
if (foreignKey.indexOf(ownerKey) === -1) {
85+
foreignKey.push(ownerKey)
86+
}
87+
}
88+
89+
/**
90+
* Set the constraints for an eager load of the relation.
91+
*/
92+
addEagerConstraints(query: Query, models: Collection): void {
93+
query.whereIn(this.ownerKey, this.getEagerModelKeys(models))
94+
}
95+
96+
/**
97+
* Gather the keys from a collection of related models.
98+
*/
99+
protected getEagerModelKeys(models: Collection): (string | number)[] {
100+
return models.reduce<(string | number)[]>((keys, model) => {
101+
return [...keys, ...model[this.foreignKey]]
102+
}, [])
103+
}
104+
105+
/**
106+
* Match the eagerly loaded results to their parents.
107+
*/
108+
match(relation: string, models: Collection, results: Collection): void {
109+
const dictionary = results.reduce<Record<string, Model>>((dic, result) => {
110+
dic[result[this.ownerKey]] = result
111+
112+
return dic
113+
}, {})
114+
115+
models.forEach((model) => {
116+
const relatedModels = this.getRelatedModels(
117+
dictionary,
118+
model[this.foreignKey]
119+
)
120+
121+
model.$setRelation(relation, relatedModels)
122+
})
123+
}
124+
125+
/**
126+
* Get all related models from the given dictionary.
127+
*/
128+
protected getRelatedModels(
129+
dictionary: Record<string, Model>,
130+
keys: (string | number)[]
131+
): Model[] {
132+
return keys.reduce<Model[]>((items, key) => {
133+
const item = dictionary[key]
134+
135+
item && items.push(item)
136+
137+
return items
138+
}, [])
139+
}
140+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Model } from '../../../Model'
2+
import { PropertyDecorator } from '../../Contracts'
3+
4+
/**
5+
* Create a has-many-by attribute property decorator.
6+
*/
7+
export function HasManyBy(
8+
related: () => typeof Model,
9+
foreignKey: string,
10+
ownerKey?: string
11+
): PropertyDecorator {
12+
return (target, propertyKey) => {
13+
const self = target.$self()
14+
15+
self.setRegistry(propertyKey, () =>
16+
self.hasManyBy(related(), foreignKey, ownerKey)
17+
)
18+
}
19+
}

0 commit comments

Comments
 (0)