Tricky situations with traits/afterCreates & Factory API
See original GitHub issueIn 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:
- Created 4 years ago
- Reactions:1
- Comments:5 (5 by maintainers)
Top GitHub Comments
@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.
Association taking a function might actually be an interesting solution that also works for hasMany, though.