question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Tricky situations with traits/afterCreates & Factory API

See original GitHub issue

In https://github.com/miragejs/miragejs/pull/187 @krasnoukhov raised some clunky parts of the current Factory API.

Let’s say we have the following models:

// models/comment.js
export default Model.extend({
  post: belongsTo()
})

// models/post.js
export default Model.extend({
  comments: hasMany()
})

A comment always has a post, so we define the following comment factory:

// factories/comment.js
export default Factory.extend({
  afterCreate(comment, server) {
    comment.update({
      post: server.create('post')
    })
  }
})

That way in tests, we can write server.create('comment') and always get a valid comment (i.e. one that has a post associated with it).

But we also want to be able to override the default post on our own at the calling site, so we can do this:

let publishedPost = server.create('post', { published: true })
server.create('comment', { post: publishedPost });

Right now this won’t work, because the afterCreate hook on our factory definition always gets called after the server.create command is finished. So we’ll override any overrides with our comment.update command in afterCreate.

To fix this, we can add some logic to our afterCreate hook:

// factories/comment.js
export default Factory.extend({
  afterCreate(comment, server) {
    if (!comment.post) {
      comment.update({
        post: server.create('post')
      })
    }
  }
})

The hook now first checks to see if the comment has an associated post, and only creates one if it doesn’t.

Now our callers can do

server.create('comment') // gets the default post
server.create('comment', { post: publishedPost }) // gets our overridden post

and get what they expect. (Note that this is what the association() helper does by default. Note also it only works for belongsTo relationships.)

Here’s the wrinkle. Let’s say we add a trait.

// factories/comment.js
export default Factory.extend({
  afterCreate(comment, server) {
    if (!comment.post) {
      comment.update({
        post: server.create('post')
      })
    }
  },

  withPublishedPost: trait({
    afterCreate(comment, server) {
      if (!comment.post) {
        comment.update({
          post: server.create('post', { published: true })
        })
      }
    }
  })
})

If we wrote it like this, server.create('comment', 'withPublishedPost') actually wouldn’t work as we expect. That’s because the root afterCreate runs first, which creates the post. Then when the trait’s afterCreate runs, the comment has a post, so comment.post is truthy and our extra code never runs.

So, we could omit the check from our trait’s afterCreate and write it like this:

// factories/comment.js
export default Factory.extend({
  afterCreate(comment, server) {
    if (!comment.post) {
      comment.update({
        post: server.create('post')
      })
    }
  },

  withPublishedPost: trait({
    afterCreate(comment, server) {
      comment.update({
        post: server.create('post', { published: true })
      })
    }
  })
})

but then we’ve introduced two new problems. First, server.create('comment', 'withPublishedPost') now creates two posts, one from the root afterCreate and another from the withPublishedPost trait. Even though the comment is correctly associated with the second one, it’s less than ideal that an extra post is created for no reason.

Second, this usage won’t work as expected:

server.create('comment', 'withPublishedPost', { post: myTestsPublishedPost })

because again, the afterCreate hook runs after the rest of the server.create logic has been resolved.

The only real escape hatch today is to do something like this:

let comment = server.create('comment', 'withPublishedPost')
comment.update({ post: myTestsPublishedPost });

but this is pretty awkward, and moreover we’ve now created 3 posts when we only really wanted 1.

(FWIW this example is a bit contrived, as one could reasonably ask, “Why use the withPublishedPost trait if you’re just going to be overriding the post? Couldn’t you just omit the trait then the override would work as expected?” But you can imagine a trait that does more work than this, and that a test writer would want to use, apart from some relationship that they want to override.)


So that’s the problem, and we need to address it!

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:1
  • Comments:5 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
samselikoffcommented, Nov 5, 2019

@krasnoukhov The problem is the helper should really be sugar for the lower-level afterCreate API; also, there is no equivalent for hasMany.

In my mind the fact that this all cannot be accomplished with afterCreate is a gap in the API that needs to be addressed, rather than papered over with an expansion to the association helper.

1reaction
samselikoffcommented, Nov 5, 2019

Association taking a function might actually be an interesting solution that also works for hasMany, though.

Read more comments on GitHub >

github_iconTop Results From Across the Web

No results found

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found