miragejs / discuss

Ask questions, get help, and share what you've built with Mirage
MIT License
2 stars 0 forks source link

HasManyThroughAssociation #36

Open jamesarosen opened 6 years ago

jamesarosen commented 6 years ago

I wrote a HasManyThroughAssociation. It's not bullet-proof and I'm sure it could be improved, but it seems to be working. Most of it is copied from the HasManyAssociation with a few changes:

import { camelize } from '@ember/string'
import { singularize } from 'ember-inflector'
import Association from 'ember-cli-mirage/orm/associations/association'
import Collection from 'ember-cli-mirage/orm/collection'
import { toCollectionName } from 'ember-cli-mirage/utils/normalize-name'

export default function(throughAssociationName) {
  return new HasManyThroughAssociation(throughAssociationName)
}

// Adapted from ember-cli-mirage/orm/associations/has-many
class HasManyThroughAssociation extends Association {
  constructor(throughAssociationName) {
    super({ polymorphic: false, throughAssociationName })
  }

  /**
   * @method getForeignKeyArray
   * @return {Array} Array of camelized model name of associated objects
   * and foreign key for the object owning the association
   * @public
   */
  getForeignKeyArray() {
    return [camelize(this.ownerModelName), this.getForeignKey()]
  }

  /**
   * @method getForeignKey
   * @return {String} Foreign key for the object owning the association
   * @public
   */
  getForeignKey() {
    return `${singularize(camelize(this.key))}Ids`
  }

  /**
   * Registers has-many association defined by given key on given model,
   * defines getters / setters for associated records and associated records' ids,
   * adds methods for creating unsaved child records and creating saved ones
   *
   * @method addMethodsToModelClass
   * @param {Function} ModelClass
   * @param {String} key
   * @public
   */
  addMethodsToModelClass(ModelClass, key) {
    let modelPrototype = ModelClass.prototype
    let association = this
    let foreignKey = this.getForeignKey()
    let associationHash = { [key]: this }

    modelPrototype.hasManyAssociations = Object.assign(
      modelPrototype.hasManyAssociations,
      associationHash
    )

    // Add to target's dependent associations array
    this.schema.addDependentAssociation(this, this.modelName)

    // TODO: look how this is used. Are these necessary, seems like they could be gotten from the above?
    // Or we could use a single data structure to store this information?
    modelPrototype.associationKeys.push(key)
    modelPrototype.associationIdKeys.push(foreignKey)

    // This is the most significant change from HasManyAssociation. Instead of looking
    // up IDs from the database, we map them from the through-association.
    Object.defineProperty(modelPrototype, foreignKey, {
      /*
        object.childrenIds
          - returns an array of the associated children's ids
      */
      get() {
        return this[association.opts.throughAssociationName].models
          .map(throughModel => {
            return throughModel[singularize(key)]
          })
          .filter(Boolean)
          .mapBy('id')
      },

      set() {
        throw new Error(
          `${key} is a has-many-through relationship; use ${this.throughAssociationName}Ids`
        )
      },
    })

    Object.defineProperty(modelPrototype, key, {
      /*
        object.children
          - returns an array of associated children
      */
      get() {
        this._tempAssociations = this._tempAssociations || {}
        let collection = null

        if (this._tempAssociations[key]) {
          collection = this._tempAssociations[key]
        } else {
          if (this[foreignKey]) {
            collection = association.schema[toCollectionName(association.modelName)].find(
              this[foreignKey]
            )
          } else {
            collection = new Collection(association.modelName)
          }
        }

        this._tempAssociations[key] = collection

        return collection
      },

      set() {
        throw new Error(
          `${key} is a has-many-through relationship; use ${this.throughAssociationName}`
        )
      },
    })
  }

  /**
   *
   *
   * @public
   */
  disassociateAllDependentsFromTarget(model) {
    let owner = this.ownerModelName
    let fk

    if (this.isPolymorphic) {
      fk = { type: model.modelName, id: model.id }
    } else {
      fk = model.id
    }

    let dependents = this.schema[toCollectionName(owner)].where(potentialOwner => {
      let currentIds = potentialOwner[this.getForeignKey()]

      // Need this check because currentIds could be null
      return (
        currentIds &&
        currentIds.find(id => {
          if (typeof id === 'object') {
            return id.type === fk.type && id.id === fk.id
          } else {
            return id === fk
          }
        })
      )
    })

    dependents.models.forEach(dependent => {
      dependent.disassociate(model, this)
      dependent.save()
    })
  }
}

I haven't written any tests. This issue is meant to be a starting point for someone who wants to drive the feature to completion.

samselikoff commented 4 years ago

FYI: Transferred this to our Discuss repo, our new home for more open-ended conversations about Mirage!

If things become more concrete + actionable we can create a tracking issue in the main repo.