Thinking about conditional linking
In certain apps, there's a need to show data from related resources that might or might not exist. One example that comes to mind is a wiki where an article can link to other articles which might not be present.
In my case, I knew very early on that I'd need this functionality. I want to present chess games from players who might not be in the database. And if they are not, I still want to show their names, at a minimum. If they are, I need their names link to the player's details page. The same goes for the tournament relation of a game, or the winner relation (a player) of a tournament.
I can also imagine that I'd like to introduce the wiki-like functionality of linking to the new page for a non-existent resource so that the player (tournament, etc.) can be created on-the-fly.
The current (non-)solution
I'd decided to put off this problem by introducing what I named "raw" attributes. So if a StandingItem
(the final standings of a tournament is of this class) doesn't have a related player
, the server sends a playerRawName
for display. Otherwise, the server sends the relationship and the client app can display player.name
, and create a link to the player.
I see two problems with this approach.
First, I have to send a raw attribute for each thing I want to display. Besides the player's name, I could also want to display their Elo points, age, and so on. That would lead to an explosion of raw attributes.
Second, it makes client code (needlessly, I hope) complicated. I'll show an example below.
Consequently, when designing a better API, my goal was to eliminate raw attributes and to simplify app layer logic in the client (data mangling in the adapter/serializer layer seems fine).
Simplifying logic in the client
I already have some code that simplifies logic in the template.
For example, for the player's name in StandingItem, I created a playerName
attribute:
export default class StandingItemModel extends Model {
@belongsTo({ async: false }) player
@attr() playerRawName
// (...)
@or('player.name', 'playerRawName') playerName;
}
Which I could then use in the template as follows:
{{#let standingItem.player standingItem.playerName as |player name|}}
{{#if player}}
{{link-to name "player" player.id class="text-grey-darkest"}}
{{else}}
{{name}}
{{/if}}
{{/let}}
I'd like to reduce the knowledge the client has about whether a related resource is present. Checking if each "raw" attribute if it exists and if not, displaying the appropriate attribute from the related resource seems inelegant.
Ideally, I'd be able to write player.name
in the template without checking anything in the client.
I'm okay with needing to check if I need to render a link (in case the record exists) or just display the attribute if it doesn't, but I wouldn't like to have more differences in handling these two cases.
Once an attribute, once a relationship?
In order to do that, the server would have to respond differently. This makes a lot of sense as it's the server that has the differentiating piece of information – the presence or absence of the record.
If the related resource exists, the server would respond as follows:
{
id: "10",
type: "standings-items",
attributes: {...},
relationships: {
player: {
data: {
id: "4",
type: "players"
},
}
}
}
And if it doesn't, the response to have the following form:
{
id: "11",
type: "standings-items",
attributes: {
player: {
name: "Leinier Dominguez Perez",
// eloPoints: ...
}
},
relationships: {
player: {
data: null
}
}
}
However, that will not fly with Ember Data, as I cannot have both a player
relationship in some cases and an attribute (which is a POJO) in others.
Can I transform (normalize) the response such that player.name
will always render the right thing? I doubt it because player
is still a belongsTo
in the model class and I can't just overwrite it.
What I could do is to create a separate class for players that don't exist in the database, for example, SimplePlayer
. In the normalizeResponse
hook, I could check if the response has a related player
. If it does, I do nothing. If it doesn't, I mangle the response as follows before passing it on to ED to create instances and relationships out of it:
{
id: "11",
type: "standings-items",
attributes: {
hasRelatedPlayer: false,
// (...)
},
relationships: {
player: {
data: {
id: <generated-guid>, type: "simple-players"
}
}
},
included: [
{
id: <the-same-generated-guid>,
type: "simple-players",
attributes: {
name: "Leinier Dominguez Perez"
}
}
]
}
Come to think of it, I could (should?) probably already do the mangling on the server-side.
What else do I need to make this work?
- The
player
relationship needs to be polymorphic but that shouldn't be hard to do. - When checking if I need to render a link, the
hasRelatedPlayer
attribute should be used (which should default to true). I could also check if the related player record is of typePlayer
(notSimplePlayer
) but that seems wrong. If it's a polymorphic relationship, let's not inquire what type it is.
To me it seems like I'm at a point where I have a workable idea that I can start acting on. It might turn out there's a better solution or that it will not work the way I imagined but I have an urge to dive in.