vue-mc icon indicating copy to clipboard operation
vue-mc copied to clipboard

Better support for nested models and collections

Open rtheunissen opened this issue 6 years ago • 14 comments

At the moment the support for nested structures is limited. We'd like to improve this as it's a common situation and should in theory work as you would expect. This issue is a place to collect all the things we'd like to achieve when we get to improving this support.

  • [ ] Errors should be set on the child, not with dot notation on the parent. (or both?)
  • [ ] Determine the difference between parent.saved('child.attr') and parent.child.saved('attr')
  • [ ] Provide a nice mechanism to convert nested objects to models

rtheunissen avatar Mar 04 '18 04:03 rtheunissen

@burningbman brought up some valuable feedback on how we might implement this. See #46, but specifically:

  • A native getter/setter for when nesting a model as an attribute.
  • An attribute (nested model) only reporting the changed state if replaced rather than modified (not sure how this could be implemented to be honest, as it isn't really within scope of anything else the library has been doing, that I recall anyway).
  • The ID of the attribute being passed with the model. Relationship handling via something like foreign keys would give an idea of implementation.

Acen avatar May 14 '18 02:05 Acen

As a rough draft of the last bullet point, I overloaded getSaveData on my custom model.

ExtendedModel.prototype.getSaveData = function() {
  let attrs = VueMC.Model.prototype.getSaveData.call(this)
  let keys = Object.keys(attrs)
  keys.forEach(function(key) {
    let val = attrs[key]
    if (val instanceof VueMC.Model) {
      attrs[key] = val.get(val.getOption('identifier'))
    }
  })
  return attrs
}

burningbman avatar May 14 '18 15:05 burningbman

So I'm looking for a library that does exactly what VueMC does: extracting away the API.

Since nested models make up a big part of my API, this really is an important part. For now, this issue seems like a showstopper in using VueMC.

Any idea if/when there will be a better implementation for nesting?

MichMich avatar Nov 09 '18 13:11 MichMich

@MichMich There are two stages here: 1) plan it, 2) implement it. I'm not confident that we know what the solution is yet, so while the implementation might be quick, it's difficult to estimate how long it would take to come up with a solution.

Can you give me an example of one of your model schema's?

The API for this should be as simple as:

delegates() {
    "a.b": ModelA,
    "x.y.*": ModelB,
}

I think if we can come up with an API that is "acceptable", we're good to go. This would only solve 1 of the 3 points laid out in the opening statement. I don't believe the other two are difficult to solve though:

Errors should be set on the child, not with dot notation on the parent. (or both?)

Should be set on the child, such that parent.errors is empty but parent.child.errors is populated.

Determine the difference between parent.saved('child.prop') and parent.child.saved('prop')

These should both be supported because they are not equivalent. This first uses the saved child's property, and the second uses the active child's saved property. This is a bit confusing but it will be very difficult to avoid that, unless we disallow nesting that isn't delegated (I don't want to do this though because it could force a lot of boilerplate).

rtheunissen avatar Nov 09 '18 22:11 rtheunissen

Hi ! Is there any update regarding the support of nested models and collections?

I am currently working with nested Models and I would like to bring my 2 cents. In my case, I am working with Profile Model with a metas property that represent a Meta Collection.

Model:

defaults(){
        return {
            profile_id : null,
            // other profile atts
            metas:new MetaCollection(),
        }
}

My endpoints: /profile /profile/{profile_id} /profile/{profile_id}/meta /profile/{profile_id}/meta/{meta_id}

Upon save(), we could execute the parent model first if it had change and all the changed child Models individually afterward in a Promise.all. This would keep the Model/Collection and API structure clean from my perspective.

The downside that could occur would be:

  • multiple AJX/API call
  • If child model fail on saving, it would create a conflict state.

Thank you!

nomadinteractif avatar Dec 07 '18 19:12 nomadinteractif

Could https://github.com/vuex-orm/vuex-orm be used as inspiration?

We're currently using vue-mc but would like to have the type of relation mapping that vuex-orm brings. On the other hand, vuex-orm doesn't provide any REST API interactions, like vue-mc does.

Sarke avatar May 08 '19 02:05 Sarke

It is a shame there is no support for relationships as this really is a requirement and a definite show-stopper without. It's only half the solution. As for an API, what about providing a very high-level definition? (see user and comments below).

class Task extends Model {
    // Default attributes that define the "empty" state.
    defaults() {
        return {
            id:   null,
            name: '',
            done: false,
            user: User,
            comments: Comment
        }
    }
}

Could you then check the types for those fields, determine the model to use and map the data that way? You might not want to check types for all fields, so you could use a convention to better distinguish relationships -- another attribute, separate function or more indication in the attribute key itself (belongsToUser/belongs_to_user, hasManyComments/has_many_comments)..

Regardless, I don't think it would be a mistake to use a standard has/belongs kind of relationship model as many developers are already familiar with it. Baking that into the interface would make it easy to follow..

function relationships() {
  return {
    user: User,
    comments: Comment
  }
}

// or

function defaults() {
  return {
    attribute: 'example',
    relationships: {
      user: User
    }
  }
}

// with options

relationships: {
  user: { model: User, foreign_key: 'etc' }
}

// abstracted into function

relationships: {
  hasMany(Comment, { my: options }),
  hasOne(Model)
}

Just a few ideas.

I think some processing on the relationship identifiers/keys would go a long way if there was a convention in place for plural vs singular naming if you used keys. However, I reckon the function approach is superior because it allows you to express the relationships and is far easier to expand.

I couldn't speak for the internal implementation, this is just an interface I'd be very happy to have at my fingertips right now.

damien-roche avatar Jun 29 '19 03:06 damien-roche

I extended Model like this for a simple relation:

export default class extends Model {

    /**
     * Creates a new instance, called when using 'new'.
     *
     * @param  {Object}     [attributes]  Model attributes
     * @param  {Collection} [collection]  Collection that this model belongs to.
     * @param  {Object}     [options]     Options to set on the model.
     */
    constructor(attributes = {}, collection = null, options = {}) {
        super(attributes, collection, options);
        this._relations = {};
        this.init();
    }

    init() {
        ;
    }

    relation(relationName, findCallback) {
        if (typeof this._relations[relationName] === 'undefined') {
            let storeData = store.get(relationName);
            if (storeData.length === 0) {
                return null;
            }

            this._relations[relationName] = storeData.find(findCallback);
            if (this._relations[relationName] === undefined) {
                this._relations[relationName] = null;
            }
        }

        return this._relations[relationName];
    }
}

The init() is just there so you can more easily add things to the constructor. All I really need to do is add this._relations = {};.

Then, in the model for a particular resource, I can just do this:

    get categoryModel() {
        return this.relation('categories', Category => Category.id === this.categoryId);
    }

Nothing crazy, just a simple way to do it. The relation is cached, and there's nothing invalidating that cache on update, but other than that it's works well.

Sarke avatar Jun 30 '19 01:06 Sarke

@Sarke How does the store behave? Is it a Vuex persistent store?

thisninja avatar Jul 10 '19 15:07 thisninja

It can be, but any kind of key-value store will work.

Sarke avatar Jul 10 '19 20:07 Sarke

Any news on this? Is this project active? I really need that feature

yudi-glive avatar Oct 06 '19 19:10 yudi-glive

Is this project active?

Absolutely. This is in the works as part of 2.0 because it will break a lot of existing functionality. The progress is slow for now, but definitely still active.

rtheunissen avatar Oct 08 '19 18:10 rtheunissen

I implemented a sort of relation system through the mutations. The only thing that doesn't work is recursive validation because the child model returns {} for passed validation and the parent model sees that as a fail. Doesn't that cause problems for any kind of nested validation though?

nmsmith22389 avatar Oct 10 '19 03:10 nmsmith22389

I just started using Vue and want to use vue-mc as the data model for the front end. Up until I was doing a fetch everything was working beautifully. However, I am using nested collections/models and, while everything loads very well and is perfectly reactive, the save function does not work. It immediately throws errors. I use mutations to load all the data, but I cannot get around the save function as it throws an error before calling any of the collections'/models' functions.

At this point I am only using a single level of nesting. I reproduced the errors with very simple collections/models. Here are the stack traces I got.

When I use a nested model:

Error: Can't use reserved attribute name '_listeners' at ChildModel.registerAttribute (vue-mc.es.js:10539) at ChildModel.set (vue-mc.es.js:10581) at vue-mc.es.js:10572 at vue-mc.es.js:2692 at baseForOwn (vue-mc.es.js:3261) at vue-mc.es.js:3280 at forEach (vue-mc.es.js:3354) at ChildModel.set (vue-mc.es.js:10571) at ChildModel.assign (vue-mc.es.js:10424) at new Model (vue-mc.es.js:10168)

When I use a nested collection:

Error: Expected a model, plain object, or array of either at Collection.add (vue-mc.es.js:7819) at arrayMap (vue-mc.es.js:1187) at map (vue-mc.es.js:4569) at Collection.add (vue-mc.es.js:7808) at new Collection (vue-mc.es.js:7499) at theItems (PrimaryAssessment.vue:63) at vue-mc.es.js:9705 at ParentModel.mutated (vue-mc.es.js:10462) at vue-mc.es.js:10479 at vue-mc.es.js:2692

I would really love to continue using vue-mc. Is there an ETA on when support for nested collections and models will be released?

rajeevayer avatar Dec 20 '20 03:12 rajeevayer