Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/content/1.guide/3.relationships/4.many-to-many.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,37 @@ user.roles.forEach((role) => {
```

Notice that each `Role` model we retrieve is automatically assigned a `pivot` attribute. This attribute contains a model representing the intermediate model and may be used like any other model.

### Customizing The `pivot` Attribute Name

As noted earlier, attributes from the intermediate model may be accessed on models using the `pivot` attribute. However, you are free to customize the name of this attribute to better reflect its purpose within your application.

For example, if your application contains users that may subscribe to podcasts, you probably have a many-to-many relationship between users and podcasts. If this is the case, you may wish to rename your intermediate table accessor to `subscription` instead of `pivot`. This can be done using the `as` method when defining the relationship:

```js
class User extends Model {
static entity = 'users'

static fields () {
return {
id: this.attr(null),
podcasts: this.belongsToMany(
Podcast,
Subscription,
'user_id',
'podcast_id'
).as('subscription')
}
}
}
```

Once this is done, you may access the intermediate table data using the customized name.

```js
const user = useRepo(User).with('podcasts').first()

user.podcasts.forEach((podcast) => {
console.log(podcast.subscription)
})
```
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class User extends Model {

@Attr(null) declare id: number | null
@BelongsToMany(() => Role, () => RoleUser, 'user_id', 'role_id') declare roles: Role[]
// or if you have other pivot key
// @BelongsToMany(() => Role, { as: 'userPivot', model: () => RoleUser }, 'user_id', 'role_id')
}
````

Expand All @@ -45,7 +47,10 @@ class User extends Model {
````ts
function belongsToMany(
related: typeof Model,
pivot: typeof Model,
pivot: (() => typeof Model) | {
as: string
model: () => typeof Model
},
foreignPivotKey: string,
relatedPivotKey: string,
parentKey?: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ class User extends Model {
````ts
function morphToMany(
related: typeof Model,
pivot: typeof Model,
pivot: (() => typeof Model) | {
as: string
model: () => typeof Model
},
relatedId: string,
id: string,
type: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ class Tag extends Model {
````ts
function morphedByMany(
related: typeof Model,
pivot: typeof Model,
pivot: (() => typeof Model) | {
as: string
model: () => typeof Model
},
relatedId: string,
id: string,
type: string,
Expand Down
2 changes: 1 addition & 1 deletion packages/pinia-orm/.eslintcache

Large diffs are not rendered by default.

11 changes: 2 additions & 9 deletions packages/pinia-orm/src/model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@ export type WithKeys<T> = { [P in keyof T]: T[P] extends (Model | null) | Model[
// export type WithKeys<T> = { [P in keyof T]: T[P] extends Model[] ? P : never }[keyof T];

export class Model {
// [s: keyof ModelFields]: any
pivot?: any

declare _meta: undefined | MetaValues
/**
* The name of the model.
Expand Down Expand Up @@ -981,13 +978,9 @@ export class Model {
/**
* Set the given relationship on the model.
*/
$setRelation (relation: string, model: Model | Model[] | null): this {
if (relation.includes('pivot')) {
this.pivot = model
return this
}
$setRelation (relation: string, model: Model | Model[] | null, isPivot = false): this {
// @ts-expect-error Setting model as field
if (this.$fields()[relation]) { this[relation as keyof this] = model }
if (this.$fields()[relation] || isPivot) { this[relation as keyof this] = model }

return this
}
Expand Down
13 changes: 11 additions & 2 deletions packages/pinia-orm/src/model/attributes/relations/BelongsToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class BelongsToMany extends Relation {
* Attach the parent type and id to the given relation.
*/
attach (record: Element, child: Element): void {
const pivot = child.pivot ?? {}
const pivot = child[this.pivotKey] ?? {}
pivot[this.foreignPivotKey] = record[this.parentKey]
pivot[this.relatedPivotKey] = child[this.relatedKey]
child[`pivot_${this.relatedPivotKey}_${this.pivot.$entity()}`] = pivot
Expand Down Expand Up @@ -110,7 +110,7 @@ export class BelongsToMany extends Relation {
if (!pivot) { return }

const relatedModelCopy = relatedModel.$newInstance(relatedModel.$toJson(), { operation: undefined })
relatedModelCopy.$setRelation('pivot', pivot)
relatedModelCopy.$setRelation(this.pivotKey, pivot, true)
relationResults.push(relatedModelCopy)
})
parentModel.$setRelation(relation, relationResults)
Expand All @@ -121,4 +121,13 @@ export class BelongsToMany extends Relation {
* Set the constraints for the related relation.
*/
addEagerConstraints (_query: Query, _collection: Collection): void {}

/**
* Specify the custom pivot accessor to use for the relationship.
*/
as (accessor: string): this {
this.pivotKey = accessor

return this
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class HasManyThrough extends Relation {
*/
protected buildDictionary (throughResults: Collection<any>, results: Collection<any>): Dictionary {
return this.mapToDictionary(throughResults, (throughResult) => {
return [throughResult[this.firstKey as keyof Model] as string, results[throughResult[this.secondLocalKey as keyof Model] as number]]
return [throughResult[this.firstKey as keyof Model] as unknown as string, results[throughResult[this.secondLocalKey as keyof Model] as unknown as number]]
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class MorphMany extends Relation {
*/
protected buildDictionary (results: Collection<any>): Dictionary {
return this.mapToDictionary(results, (result) => {
return [result[this.morphId as keyof Model] as string, result]
return [result[this.morphId as keyof Model] as unknown as string, result]
})
}

Expand Down
4 changes: 2 additions & 2 deletions packages/pinia-orm/src/model/attributes/relations/MorphTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export class MorphTo extends Relation {
const dictionary = this.buildDictionary(query, models)

models.forEach((model) => {
const type = model[this.morphType as keyof Model] as string
const id = model[this.morphId as keyof Model] as number
const type = model[this.morphType as keyof Model] as unknown as string
const id = model[this.morphId as keyof Model] as unknown as number

const related = dictionary[type]?.[id] ?? null

Expand Down
13 changes: 11 additions & 2 deletions packages/pinia-orm/src/model/attributes/relations/MorphToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class MorphToMany extends Relation {
* Attach the parent type and id to the given relation.
*/
attach (record: Element, child: Element): void {
const pivot = child.pivot ?? {}
const pivot = child[this.pivotKey] ?? {}
pivot[this.morphId] = record[this.parentKey]
pivot[this.morphType] = this.parent.$entity()
pivot[this.relatedId] = child[this.relatedKey]
Expand Down Expand Up @@ -116,7 +116,7 @@ export class MorphToMany extends Relation {
const pivot = pivotModels[`[${parentModel[this.parentKey]},${relatedModel[this.relatedKey]},${this.parent.$entity()}]`]?.[0] ?? null

const relatedModelCopy = relatedModel.$newInstance(relatedModel.$toJson(), { operation: undefined })
relatedModelCopy.$setRelation('pivot', pivot)
relatedModelCopy.$setRelation(this.pivotKey, pivot, true)

if (pivot) { relationResults.push(relatedModelCopy) }
})
Expand All @@ -128,4 +128,13 @@ export class MorphToMany extends Relation {
* Set the constraints for the related relation.
*/
addEagerConstraints (_query: Query, _collection: Collection): void {}

/**
* Specify the custom pivot accessor to use for the relationship.
*/
as (accessor: string): this {
this.pivotKey = accessor

return this
}
}
23 changes: 14 additions & 9 deletions packages/pinia-orm/src/model/attributes/relations/MorphedByMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class MorphedByMany extends Relation {
* Attach the parent type and id to the given relation.
*/
attach (record: Element, child: Element): void {
const pivot = record.pivot ?? {}
const pivot = record[this.pivotKey] ?? {}
pivot[this.morphId] = child[this.relatedKey]
pivot[this.morphType] = this.related.$entity()
pivot[this.relatedId] = record[this.parentKey]
Expand Down Expand Up @@ -111,22 +111,27 @@ export class MorphedByMany extends Relation {
.get<'group'>()

models.forEach((parentModel) => {
const relationResults: Model[] = []
const resultModelIds = this.getKeys(pivotModels[`[${parentModel[this.parentKey]},${this.related.$entity()}]`] ?? [], this.morphId)
const relatedModelsFiltered = relatedModels.filter(filterdModel => resultModelIds.includes(filterdModel[this.relatedKey]))

relatedModelsFiltered.forEach((relatedModel) => {
const pivot = (pivotModels[`[${parentModel[this.parentKey]},${this.related.$entity()}]`] ?? []).find(pivotModel => pivotModel[this.morphId] === relatedModel[this.relatedKey]) ?? null
const relatedModelCopy = relatedModel.$newInstance(relatedModel.$toJson(), { operation: undefined })
if (pivot) { relatedModelCopy.$setRelation('pivot', pivot) }
relationResults.push(relatedModelCopy)
})
parentModel.$setRelation(relation, relationResults)
const pivot = (pivotModels[`[${parentModel[this.parentKey]},${this.related.$entity()}]`] ?? [])?.[0] ?? null
if (pivot) { parentModel.$setRelation(this.pivotKey, pivot, true) }

parentModel.$setRelation(relation, relatedModelsFiltered)
})
}

/**
* Set the constraints for the related relation.
*/
addEagerConstraints (_query: Query, _collection: Collection<any>): void {}

/**
* Specify the custom pivot accessor to use for the relationship.
*/
as (accessor: string): this {
this.pivotKey = accessor

return this
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,6 @@ export abstract class Relation extends Attribute {
* Get the index key defined by the primary key or keys (composite)
*/
protected getResolvedKey (model: Model, key: PrimaryKey): string {
return isArray(key) ? `[${key.map(keyPart => model[keyPart as keyof Model]).toString()}]` : model[key as keyof Model]
return isArray(key) ? `[${key.map(keyPart => model[keyPart as keyof Model] as unknown as string).toString()}]` : model[key as keyof Model] as unknown as string
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import type { PropertyDecorator } from '../../Contracts'
*/
export function BelongsToMany (
related: () => typeof Model,
pivot: () => typeof Model,
pivot: (() => typeof Model) | {
as: string
model: () => typeof Model
},
foreignPivotKey: string,
relatedPivotKey: string,
parentKey?: string,
Expand All @@ -15,8 +18,11 @@ export function BelongsToMany (
return (target, propertyKey) => {
const self = target.$self()

self.setRegistry(propertyKey, () =>
self.belongsToMany(related(), pivot(), foreignPivotKey, relatedPivotKey, parentKey, relatedKey),
self.setRegistry(propertyKey, () => {
if (typeof pivot === 'function') { return self.belongsToMany(related(), pivot(), foreignPivotKey, relatedPivotKey, parentKey, relatedKey) }

return self.belongsToMany(related(), pivot.model(), foreignPivotKey, relatedPivotKey, parentKey, relatedKey).as(pivot.as)
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import type { PropertyDecorator } from '../../Contracts'
*/
export function MorphToMany (
related: () => typeof Model,
pivot: () => typeof Model,
pivot: (() => typeof Model) | {
as: string
model: () => typeof Model
},
relatedId: string,
id: string,
type: string,
Expand All @@ -16,8 +19,10 @@ export function MorphToMany (
return (target, propertyKey) => {
const self = target.$self()

self.setRegistry(propertyKey, () =>
self.morphToMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey),
)
self.setRegistry(propertyKey, () => {
if (typeof pivot === 'function') { return self.morphToMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey) }

return self.morphToMany(related(), pivot.model(), relatedId, id, type, parentKey, relatedKey).as(pivot.as)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import type { PropertyDecorator } from '../../Contracts'
*/
export function MorphedByMany (
related: () => typeof Model,
pivot: () => typeof Model,
pivot: (() => typeof Model) | {
as: string
model: () => typeof Model
},
relatedId: string,
id: string,
type: string,
Expand All @@ -16,8 +19,10 @@ export function MorphedByMany (
return (target, propertyKey) => {
const self = target.$self()

self.setRegistry(propertyKey, () =>
self.morphedByMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey),
)
self.setRegistry(propertyKey, () => {
if (typeof pivot === 'function') { return self.morphedByMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey) }

return self.morphedByMany(related(), pivot.model(), relatedId, id, type, parentKey, relatedKey).as(pivot.as)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ describe('feature/relations/belongs_to_many_retrieve', () => {

@Attr() id!: number
@Str('') name!: string
@BelongsToMany(() => Role, () => RoleUser, 'user_id', 'role_id')
@BelongsToMany(() => Role, { as: 'userPivot', model: () => RoleUser }, 'user_id', 'role_id')
roles!: Role[]

declare pivot: RoleUser
}

class Role extends Model {
Expand All @@ -21,7 +23,7 @@ describe('feature/relations/belongs_to_many_retrieve', () => {
@BelongsToMany(() => User, () => RoleUser, 'role_id', 'user_id')
users!: User[]

declare pivot: RoleUser
declare userPivot: RoleUser
}

class RoleUser extends Model {
Expand Down Expand Up @@ -61,9 +63,9 @@ describe('feature/relations/belongs_to_many_retrieve', () => {
assertInstanceOf(user!.roles, Role)

expect(user?.roles.length).toBe(2)
expect(user?.roles[0].pivot.level).toBe(1)
expect(user?.roles[0].userPivot.level).toBe(1)
expect(user2?.roles.length).toBe(1)
expect(user2?.roles[0].pivot.level).toBe(2)
expect(user2?.roles[0].userPivot.level).toBe(2)

const userWithoutRoles = userRepo.with('roles').find(3)
expect(userWithoutRoles?.roles.length).toBe(0)
Expand Down Expand Up @@ -91,18 +93,18 @@ describe('feature/relations/belongs_to_many_retrieve', () => {

expect(users[0].id).toBe(1)
expect(users[0].roles[0].id).toBe(1)
expect(users[0].roles[0].pivot.role_id).toBe(1)
expect(users[0].roles[0].pivot.user_id).toBe(1)
expect(users[0].roles[0].pivot.level).toBe(1)
expect(users[0].roles[0].userPivot.role_id).toBe(1)
expect(users[0].roles[0].userPivot.user_id).toBe(1)
expect(users[0].roles[0].userPivot.level).toBe(1)
expect(users[0].roles[1].id).toBe(2)
expect(users[0].roles[1].pivot.role_id).toBe(2)
expect(users[0].roles[1].pivot.user_id).toBe(1)
expect(users[0].roles[1].pivot.level).toBe(null)
expect(users[0].roles[1].userPivot.role_id).toBe(2)
expect(users[0].roles[1].userPivot.user_id).toBe(1)
expect(users[0].roles[1].userPivot.level).toBe(null)
expect(users[1].id).toBe(2)
expect(users[1].roles[0].id).toBe(1)
expect(users[1].roles[0].pivot.role_id).toBe(1)
expect(users[1].roles[0].pivot.user_id).toBe(2)
expect(users[1].roles[0].pivot.level).toBe(2)
expect(users[1].roles[0].userPivot.role_id).toBe(1)
expect(users[1].roles[0].userPivot.user_id).toBe(2)
expect(users[1].roles[0].userPivot.level).toBe(2)

const roles = useRepo(Role).with('users').get()

Expand Down
Loading