-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow for extendable Entity Loaders #110
Conversation
I'm not really sure I understand the point of this. So your example is that someone wrote an I guess I'm confused at the combination of The point of the |
These are some valid concerns. I'll try to address them as best I can, though I might have some trouble with that since you aren't wrong. I wonder if the This proposal is based on some limitations I came up against while writing my own DataSource for NeDB, but I'll use a hypothetical SQL Source to illustrate my point. Here's the issue - Suppose I'm writing a 'guild roster' command (defined in some bundle) that lists every player in a particular guild. I also have my player data on an SQL server somewhere. It is possible to dig out the SQL DataSource itself through the EntityLoader, but then I lose most of the advantages associated with (or I imagine are associated with) EntityLoaders. I also need to know how the DataSource's API works internally, and apply configuration manually in the command definition - and in every similar command. If direct access of DataSources in bundle code is the correct approach in these cases, then I'll concede that this precise change is unnecessary. I would still have no way to express a more complex query, however. My example doesn't quite show the situation you describe. The idea is that the custom loader is (typically) designed for use with one specific type of DataSource - it acts as that DataSource's interface. My 'datasource-nedb' package would define both the Loader and the Source, for example. The DataSource itself is never modified. (I may have been misleading when I said they could be defined independently for existing sources - they can, but that's not a typical use case). I did try to be careful in how a custom loader would work in practice. The "mixin" pattern ensures that the custom EntityLoader always defines at least the base fetch/update/etc functions. Requiring the game developer to set the mixin explicitly in There are probably other solutions to this. If Ranvier adopted as standard the use of query language (say, GraphQL), then the core EntityLoader could have a 'query' method that would allow for more complex interactions. This would be safer, though it then becomes incumbent on the DataSource author to resolve queries correctly (and it might still fail if we switch DataSources, though perhaps silently). I proposed this particular solution because it seemed like the simplest to implement, and the most transparent to anyone using it. I'm certainly open to other options, though. (PS - I didn't consider it when writing the example, but good point on the |
If I'm understanding correctly the gist is you may have some "thing" that is actually the equivalent of DB relation (get all players in guild) where that data/relationship may not even be in the same place where a player is stored (in a join table, for example) It's definitely an interesting problem. The solution at the moment would have to be handled with a normalized model and resolved as a filter/map as you said. Where const playerGuilds = await playerGuildLoader.fetchAll();
const result = await Promise.all(
playerGuilds
.filter(pg => pg.guild_id === targetGuildId)
.map(async pg => playerLoader.fetch(pg.player_id))
); The benefit of this is basically what I outlined before. EntityLoaders have no idea how the data is sourced/cached/persisted and they only care about one Entity at a time. The downside, as you mentioned, is that this is cumbersome at best and at worst because the loaders don't know anything about how the data is sourced and because the data sources aren't talking to each other it may be terribly inefficient. The The idea of a |
Fair enough - I'll close this PR and resubmit with just the added Just to wrap up, it sounds like keeping the EntityLoader API consistent for everyone is an important concern. That makes sense. We also seem to be on the same page regarding the current limitations of the system. Here's my understanding:
Points 2 and 3 were how I justified the ability to extend EntityLoader's methods. While EntityLoader might not know anything about where the data comes from, the developer using it kind of has to. For instance, the developer who wrote the For this reason, extending EntityLoader doesn't introduce a new class of problem, unless the possible loss of the prettier Error message is unacceptable (which I would buy). We could ignore the cases in core and say that the EntityLoader API should be homogeneous because bundle code might be shared across projects with different configurations. This is somewhat hard to defend, since DataSources can still not implement certain methods, causing some projects to break where others wouldn't. Bottom line - EntityLoaders suggest a consistent API, but don't guarantee one. Developers need to know something about the DataSource to make informed decisions about what the EntityLoader will do. Point 4 is interesting, and might actually suggest a better solution than mine. Forget my previous Players/Guild example and imagine a simpler query: "get all players under level 10". We're still stuck with const newbies = await playerLoader.fetch({ level: { $lt: 10 } }); It's ugly, and obviously not the intended use of EntityLoaders, but if I document the DataSource well enough, developers will know that this usage returns a list of players. (It also doesn't touch core, so you can't stop me. :p) I wouldn't use that approach, but it gets closer to what I think I really wanted, which was something like this: const newbies = await playerLoader.fetchAll({ level: { $lt: 10 } });
const bobsWife = await playerLoader.fetch({ spouse: "Bob" }); // assume monogamy
// Bob died :(
await playerLoader.update({spouse: "Bob"}, {spouse: null}); If we trust the developer enough to allow EntityLoader methods to accept arbitrary parameters, then we afford them a lot more expressive power while still keeping the "pretty error" behavior. It's also a lot less work on DataSource authors than my previous solution, since we don't have to write two pieces to the puzzle. Semantic issues could be addressed with additional methods like These changes only really apply to single-table queries, admittedly. Complex, join-y operations are still an issue, but I suspect that won't be 100% solvable in core no matter what we try. It is likely solvable at the DataSource level, though, since those are able see multiple EntityLoaders and coordinate them, provided the developer can sufficiently express what they want. |
Kinda. I'd phrase it as "EntityLoaders are the way to access entities. Pretend DataSources don't exist." Neither an EntityLoader or someone using an EntityLoader should need to know or care about implementation details of a
No. They only know that the I'm fully on board with the query stuff. I'd personally keep the signatures of |
From the DataSource example in the docs, we have this comment in the
I suppose this is where my confusion stems from - it's implies that the other DataSource methods aren't required to be defined. Verifying that they exist at startup would most likely be a good idea, and would ensure that the project developer doesn't need to concern themselves with whether or not any EntityLoader will throw errors on a given method. It does put the DataSource firmly in the role of implementation detail though, which is mildly disappointing on my end - I had a more feature-oriented picture in my head. As such, I won't pretend I understand the intention of this code well enough to pursue any further modifications. (For the record, I'd take Thanks for the feedback. |
This implements a new, optional feature to
DataSourceRegistry
andEntityLoaderRegistry
that allows developers to extend the behavior of the defaultEntityLoader
class. It additionally addresses one component of #96 by adding aremove
method toEntityLoader
.Current Issue
EntityLoader
defines a complete CRUD API sufficient for working with core Ranvier components, but developers may wish to make use of additional functionality provided to them by their chosen method of persistence. For example, a developer using an SQL solution may wish to query for or operate on a subset of data, which is inefficient using the providedfetchAll
andreplace
methods, particularly on large datasets.Proposed Solution
Because we can't predict what features might be available to any one backend, we can instead allow developers to define extensions to
EntityLoader
. These extensions will most often be written by DataSource authors (and included alongside it) to take advantage of their loader's unique features, but can be defined independently if desired.To enable a loader extension, we add an additional option to the
dataSources
field inranvier.json
:If
extendLoader
is set,DataSourceRegistry
will call the function withEntityLoader
to create a mixin class, which will be used for all Entity Loaders using that source. Example: