miragejs / discuss

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

Model hooks / default attributes #45

Open benswinburne opened 4 years ago

benswinburne commented 4 years ago

Is it possible to have timestamps automatically created/touched on models when created/updated?

I saw it can be done with factories when seeding and then explicitly writing handlers for all the routes, but if I want to use the nice shorthand route handlers then it'd be nice to have some hooks i can set up on the model itself. which always run when created/touched perhaps?

Something like the below maybe?

Model.extend({
  beforeCreate: model => {
    model.createdAt = new Date;
    return this;
  }
})

The following hooks may be useful.

beforeCreate
afterCreate
beforeUpdate
afterUpdate
beforeSave (both create/update)
afterSave (both create/update)
beforeDelete
afterDelete
samselikoff commented 4 years ago

Was surprised that I didn't find this existing as it's been asked several times before + is definitely a legitimate feature request.

It's also confusing as folks use factory attrs as "defaults" then wonder how to use them in route handlers. (But factory attrs serve a different purpose).

It's definitely a gap in Mirage atm – best solution today is to just make some sort of "createUser" function that has the default logic, and can be shared in route handlers and/or factories.

Hooks are a good idea. As for our priority right now, it's getting Mirage working in some different environments that it doesn't right now (e.g. Next.js) and shipping a 1.0. Then, before we add a feature like this, we need to get ES6 classes in. I think that's our rough roadmap so while I think this is an important feature it's not yet being worked on.

benswinburne commented 4 years ago

Thanks for the response, that makes sense.

With regards to your point about the factory attributes as defaults- I guess you mean something like this?

factories: {
  creative: Factory.extend({
    createdAt: faker.date.past()
  }),
},

routes() {
  this.namespace = '/';

  this.post('/creatives', function(schema, request) {
    const formData = request...;
    schema.server.create('creative', formData)
  })
},

I haven't tried the above because I ended up extending the save() method (detailed below). I didn't find the concept of factory attributes confusing but I was thrown by the fact the fixtures don't use the factories or even the models to create the data, they're just copied into the database verbatim. I'd set up some fixtures with some static data like IDs which I always wanted to know they're there then was hoping to fill in lots of arbitrary data in using factories or models. It meant eventually i had to drop the use of fixtures and just create everything with server.create().

In the mean time, I've found the below sufficient, it simply adds some attributes on.

function save() {
  const utc = new Date().toUTCString();
  const ts = new Date(utc).toISOString();

  // This allows you to seed models with a created and or updated time as there
  // is no concept of dirty/clean attributes this is the best i can get
  if (this.isNew()) {
    this.attrs.updatedAt = this.attrs.updatedAt || this.attrs.createdAt || ts;
  } else {
    this.attrs.updatedAt = ts;
  }

  this.attrs.createdAt = this.attrs.createdAt || ts;

  if (this.fks.includes('ownerId')) {
    this.attrs.ownerId = loggedInUser.id;
  }

  return Model.prototype.save.call(this);
}

new Server({
  models: {
    creative: Model.extend({
      campaigns: hasMany(),
      owner: belongsTo('user'),
      save,
    }),
  }
});
samselikoff commented 4 years ago

Sorry about the delayed response! I'm behind on issues a bit due to some consulting/training work.

With regards to your point about the factory attributes as defaults

I wasn't clear – my point was that some folks reach for using factory attributes as model defaults, but that's incorrect usage and should not be done. I was just validating that your need for model defaults is valid + a gap in Mirage today.

It meant eventually i had to drop the use of fixtures and just create everything with server.create()

I personally do this too and think it's the best approach, since using server.create lets Mirage's ORM take care of some more bookkeeping for you. As far as static IDs you always want to exist, I think there's usually better ways to achieve this, so if you're running into pain points here I'd love to hear them and possibly explore alternatives.

In the mean time, I've found the below sufficient..

Model save methods were never "meant" to be extended in userland so you might run into some unexpected behavior... you should also try to stay away from private properties like this.fks because they could change out from underneath you. (Even though you can access this.fks it's not public since it's not documented on the site. Instead you should refactor to use this.associations and then the Association API to replace your logic.)

Hopefully that helps! Let me know if you have any more questions.

cwagner22 commented 3 years ago

Any update on the recommended way to have default attributes?

samselikoff commented 3 years ago

Unfortunately not, I think functions are the best bet today. Would gladly accept PRs in this direction if someone were so inclined!