In our app, we have a number of different has-zero-or-one relationships where the foreign object may or may not exist.
For example, a Customer
may or may not have a CreditCard
on file, but it won't have more than one.
We started with something like
// app/models/customer.js
export default DS.Model.extend({
creditCard: DS.belongsTo('credit-card', { async: true }),
})
// app/routes/customer/credit-card.js
import { isNotFoundError } from 'ember-ajax/errors'
export Ember.Route.extend({
model() {
const customer = this.modelFor('customer')
return customer.get('creditCard').catch((e) => {
if (!isNotFoundError(e)) { throw e }
return this.get('store').createRecord('credit-card', { customer })
})
},
})
It's a bit cumbersome to have that error handling every time we fetch one of these models, though.
My first attempt was to use ember-data's adapter to solve this:
// app/adapters/application.js
export DS.JSONAPIAdapter.extend({
findBelongsTo(store, snapshot, url, relationship) {
return this._super(...arguments).catch((response) => {
if (isNotFoundError(response) && relationship.options.blankOn404) {
return {
data: {
id: snapshot.id,
type: relationship.type
}
}
}
throw response;
})
},
})
Then I can declare any relationship as an upsert-to-blank relationship:
// app/models/customer.js
creditCard: DS.belongsTo('credit-card', { async: true, blankOn404: true }),
And finally, I can clean up my route:
// app/routes/customer/credit-card.js
model() {
const customer = this.modelFor('customer')
return customer.get('creditCard')
}
This works great!
Except now the creditCard
record is never new. I can't, for example, do
And when I try to call creditCard.save()
, it will always make a PATCH
, never
a POST
.
I can get around this by giving the model a special id
and mixing in some
logic to the models
// app/adapters/application.js
return {
data: {
id: `${snapshot.id}--empty`,
type: relationship.type
}
}
// app/mixins/empty-support.js
export default Ember.Mixin.create({
isEmpty: Ember.computed('id', function() {
return /--empty$/.test(this.get('id'))
})
})
// app/models/credit-card.js
import EmptySupport from '../mixins/empty-support'
export default DS.Model.extend(EmptySupport)
That sort of works, but it means I also need to change the adapter to check
isEmpty
when deciding whether to do a POST
or a PATCH
.
My second idea was to create a mixin:fetch-or-build
and be more explicit about
when I'm invoking that logic:
// app/mixins/fetch-or-build.js
export default Ember.Mixin.create({
fetchOrBuild(relationshipName) {
const relationship = this.relationshipFor(relationshipName)
Ember.assert(`Cannot find relationship ${relationshipName}`, relationship)
const foreignObject = this.get(relationshipName)
if (!foreignObject.catch) { return foreignObject } // not a promise-proxy
return foreignObject.catch((e) => {
if (!isNotFoundError(e)) { throw e }
const foreignType = relationship.type
const thisType = this.constructor.modelName
const attributes = { [thisType]: this }
return this.store.createRecord(foreignType, attributes);
})
}
})
// app/models/customer.js
import FetchOrBuild from '../fetch-or-build'
export default Ember.Model.extend(FetchOrBuild, {
creditCard: DS.belongsTo('credit-card', { async: true })
})
// app/routes/customer/credit-card.js
model() {
const customer = this.modelFor('customer')
return customer.fetchOrBuild('creditCard')
}
This also works... ish.
The major downside I've found is that if I wait until after ember-data has
finished the fetch to catch the error, then the error logs to the console.
I suspect that this is because of an Ember.run.join
or an
Ember.RSVP.resolve
in ember-data's code.
Of course, we could change the server to always return a 200 for
GET /customers/CUSTOMER_ID/credit-card
, even if no object exists in the
database. The server would have to accept PATCH
es when there's no record
as well, since the 200 indicates to the outside world that the record already
exists.
This is probably the most elegant solution, but it probably isn't the easiest, especially given the large number of has-zero-or-one relationships we have in our application.
If you have other suggestions, I'd love to hear them! Comment in the doobly doo below ๐
I might approach it like this: