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
25 changes: 23 additions & 2 deletions docs/content/1.guide/3.relationships/5.polymorphic.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,30 @@ class Taggable extends Model {
}
```

### Access Intermediate Model
### Defining The Inverse Of The Relationship

To define the inverse relation to fetch related record – for this example it's for Tag model – you can use the `this.morphedByMany()` attribute.

> Since: 0.36.0+
```js
class Tag extends Model {
static entity = 'tags'

static fields () {
return {
id: this.attr(null),
name: this.attr(''),
posts: this.morphedByMany(
Post, Taggable, 'tag_id', 'taggable_id', 'taggable_type'
),
videos: this.morphedByMany(
Video, Taggable, 'tag_id', 'taggable_id', 'taggable_type'
)
}
}
}
```

### Access Intermediate Model

As the same as `belongsToMany` relationship, you may access the intermediate model for polymorphic many-to-many relationship through `pivot` attribute on the model.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: 'morphedByMany()'
description: 'Makes a "morphed by many" relation of the property. Its the inverse of `morphToMany`'
---

# `morphedByMany`

## Usage

```js
import { Model } from 'pinia-orm'
import User from './models/user'
import Video from './models/video'
import Taggable from './models/taggable'

class Tag extends Model {
static entity = 'tags'

static fields () {
return {
id: this.attr(0),
users: this.morphedByMany(User, Taggable, 'tag_id', 'taggable_id', 'taggable_type'),
videos: this.morphedByMany(Video, Taggable, 'tag_id', 'taggable_id', 'taggable_type'),
}
}
}
```

## With Decorator

````ts
import { Model } from 'pinia-orm'
import { Attr, MorphedByMany, Str } from 'pinia-orm/decorators'
import User from './models/user'
import Video from './models/video'
import Taggable from './models/taggable'

class Tag extends Model {
static entity = 'tags'

@Attr() declare id: number
@MorphedByMany(() => User, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type')
declare users: User[]
@MorphedByMany(() => Video, () => Taggable, 'tag_id', 'taggable_id', 'taggable_type')
declare videos: Video[]
declare pivot: Taggable
}
````

## Typescript Declarations

````ts
function morphedByMany(
related: typeof Model,
pivot: typeof Model,
relatedId: string,
id: string,
type: string,
parentKey?: string,
relatedKey?: string,
): MorphedByMany
````
2 changes: 1 addition & 1 deletion packages/pinia-orm/.eslintcache

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/pinia-orm/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './model/decorators/attributes/relations/HasManyThrough'
export * from './model/decorators/attributes/relations/MorphOne'
export * from './model/decorators/attributes/relations/MorphTo'
export * from './model/decorators/attributes/relations/MorphToMany'
export * from './model/decorators/attributes/relations/MorphedByMany'
export * from './model/decorators/attributes/relations/MorphMany'
export * from './model/decorators/Contracts'
export * from './model/decorators/OnDelete'
Expand Down
34 changes: 34 additions & 0 deletions packages/pinia-orm/src/model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { TypeDefault } from './attributes/types/Type'
import { HasManyThrough } from './attributes/relations/HasManyThrough'
import { MorphToMany } from './attributes/relations/MorphToMany'
import type { UidOptions } from './decorators/Contracts'
import { MorphedByMany } from './attributes/relations/MorphedByMany'

export type ModelFields = Record<string, Attribute>
export type ModelSchemas = Record<string, ModelFields>
Expand Down Expand Up @@ -441,6 +442,39 @@ export class Model {
)
}

/**
* Create a new MorphedByMany relation instance.
*/
static morphedByMany (
related: typeof Model,
pivot: typeof Model,
relatedId: string,
id: string,
type: string,
parentKey?: string,
relatedKey?: string,
): MorphToMany {
const instance = related.newRawInstance()
const model = this.newRawInstance()
const pivotInstance = pivot.newRawInstance()

parentKey = parentKey ?? model.$getLocalKey()
relatedKey = relatedKey ?? instance.$getLocalKey()

this.schemas[related.modelEntity()][`pivot_${relatedId}_${pivotInstance.$entity()}`] = new MorphOne(model, pivotInstance, id, type, relatedKey)

return new MorphedByMany(
model,
instance,
pivotInstance,
relatedId,
id,
type,
parentKey,
relatedKey,
)
}

/**
* Create a new HasMany relation instance.
*/
Expand Down
132 changes: 132 additions & 0 deletions packages/pinia-orm/src/model/attributes/relations/MorphedByMany.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { Schema as NormalizrSchema } from '@pinia-orm/normalizr'
import type { Schema } from '../../../schema/Schema'
import type { Collection, Element } from '../../../data/Data'
import type { Query } from '../../../query/Query'
import type { Model } from '../../Model'
import { Relation } from './Relation'

export class MorphedByMany extends Relation {
/**
* The pivot model.
*/
pivot: Model

/**
* The field name that contains id of the parent model.
*/
morphId: string

/**
* The field name that contains type of the parent model.
*/
morphType: string

/**
* The associated key of the relation.
*/
relatedId: string

/**
* The key name of the parent model.
*/
parentKey: string

/**
* The key name of the related model.
*/
relatedKey: string

/**
* The key name of the pivot data.
*/
pivotKey = 'pivot'

/**
* Create a new morph to many to instance.
*/
constructor (
parent: Model,
related: Model,
pivot: Model,
relatedId: string,
morphId: string,
morphType: string,
parentKey: string,
relatedKey: string,
) {
super(parent, related)

this.pivot = pivot
this.morphId = morphId
this.morphType = morphType
this.relatedId = relatedId
this.parentKey = parentKey
this.relatedKey = relatedKey
}

/**
* Get all related models for the relationship.
*/
getRelateds (): Model[] {
return [this.related, this.pivot]
}

/**
* Define the normalizr schema for the relationship.
*/
define (schema: Schema): NormalizrSchema {
return schema.many(this.related, this.parent)
}

/**
* Attach the parent type and id to the given relation.
*/
attach (record: Element, child: Element): void {
const pivot = record.pivot ?? {}
pivot[this.morphId] = child[this.relatedKey]
pivot[this.morphType] = this.related.$entity()
pivot[this.relatedId] = record[this.parentKey]
child[`pivot_${this.relatedId}_${this.pivot.$entity()}`] = pivot
}

/**
* Convert given value to the appropriate value for the attribute.
*/
make (elements?: Element[]): Model[] {
return elements
? elements.map(element => this.related.$newInstance(element))
: []
}

/**
* Match the eagerly loaded results to their parents.
*/
match (relation: string, models: Collection<any>, query: Query<any>): void {
const relatedModels = query.get(false)
const pivotModels = query
.newQuery(this.pivot.$modelEntity())
.whereIn(this.relatedId, this.getKeys(models, this.parentKey))
.whereIn(this.morphId, this.getKeys(relatedModels, this.relatedKey))
.groupBy(this.relatedId, this.morphType)
.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)
})
}

/**
* Set the constraints for the related relation.
*/
addEagerConstraints (_query: Query, _collection: Collection<any>): void {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Model } from '../../../Model'
import type { PropertyDecorator } from '../../Contracts'

/**
* Create a morph-to-many attribute property decorator.
*/
export function MorphedByMany (
related: () => typeof Model,
pivot: () => typeof Model,
relatedId: string,
id: string,
type: string,
parentKey?: string,
relatedKey?: string,
): PropertyDecorator {
return (target, propertyKey) => {
const self = target.$self()

self.setRegistry(propertyKey, () =>
self.morphedByMany(related(), pivot(), relatedId, id, type, parentKey, relatedKey),
)
}
}
Loading